new "Open" and "Save" drawing features

* Use a "drawOnYourScreen" subdir in the xdg user data dir
* Info item with the name of the last saved/open json file
* "Open drawing" sub menu with data dir content
* "Save drawing" sub menu with entry
* "Save", "Open previous" and "Open next" keybindings
This commit is contained in:
abakkk 2020-01-04 00:16:20 +01:00
parent 7c7d75a2ba
commit 17bbe345af
7 changed files with 363 additions and 19 deletions

311
draw.js
View File

@ -48,6 +48,7 @@ const _ = imports.gettext.domain(Extension.metadata["gettext-domain"]).gettext;
const GS_VERSION = Config.PACKAGE_VERSION;
const DEFAULT_FILE_NAME = 'DrawOnYourScreen';
const DATA_SUB_DIR = 'drawOnYourScreen'
const FILL_ICON_PATH = Extension.dir.get_child('icons').get_child('fill-symbolic.svg').get_path();
const STROKE_ICON_PATH = Extension.dir.get_child('icons').get_child('stroke-symbolic.svg').get_path();
@ -70,6 +71,40 @@ function getDateString() {
return `${date.format("%F")} ${date.format("%X")}`;
}
function getJsonFiles() {
let directory = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), DATA_SUB_DIR]));
if (!directory.query_exists(null))
return [];
let jsonFiles = [];
let enumerator;
try {
enumerator = directory.enumerate_children('standard::name,standard::display-name,standard::content-type,time::modified', Gio.FileQueryInfoFlags.NONE, null);
} catch(e) {
return [];
}
let i = 0;
let fileInfo = enumerator.next_file(null);
while (fileInfo) {
if (fileInfo.get_content_type().indexOf('json') != -1 && fileInfo.get_name() != `${DEFAULT_FILE_NAME}.json`) {
let file = enumerator.get_child(fileInfo);
jsonFiles.push({ name: fileInfo.get_name().slice(0, -5),
displayName: fileInfo.get_display_name().slice(0, -5),
modificationDateTime: fileInfo.get_modification_date_time(),
delete: () => file.delete(null) });
}
fileInfo = enumerator.next_file(null);
}
enumerator.close(null);
jsonFiles.sort((a, b) => {
return a.modificationDateTime.difference(b.modificationDateTime);
});
return jsonFiles;
}
// DrawingArea is the widget in which we draw, thanks to Cairo.
// It creates and manages a DrawingElement for each "brushstroke".
// It handles pointer/mouse/(touch?) events and some keyboard events.
@ -79,7 +114,7 @@ var DrawingArea = new Lang.Class({
Signals: { 'show-osd': { param_types: [GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_DOUBLE] },
'stop-drawing': {} },
_init: function(params, monitor, helper, loadJson) {
_init: function(params, monitor, helper, loadPersistent) {
this.parent({ style_class: 'draw-on-your-screen', name: params && params.name ? params.name : ""});
this.connect('repaint', this._repaint.bind(this));
@ -99,8 +134,8 @@ var DrawingArea = new Lang.Class({
this.fill = false;
this.colors = [Clutter.Color.new(0, 0, 0, 255)];
if (loadJson)
this._loadJson();
if (loadPersistent)
this._loadPersistent();
},
get menu() {
@ -591,7 +626,7 @@ var DrawingArea = new Lang.Class({
this._menu.close();
this.get_parent().set_background_color(null);
if (save)
this.saveAsJson();
this.savePersistent();
},
saveAsSvg: function() {
@ -632,10 +667,18 @@ var DrawingArea = new Lang.Class({
}
},
saveAsJson: function() {
let filename = `${DEFAULT_FILE_NAME}.json`;
let dir = GLib.get_user_data_dir();
let path = GLib.build_filenamev([dir, filename]);
_saveAsJson: function(name, notify) {
// stop drawing or writing
if (this.currentElement && this.currentElement.shape == Shapes.TEXT && this.currentElement.state == TextState.WRITING) {
this._stopWriting();
} else if (this.currentElement && this.currentElement.shape != Shapes.TEXT) {
this._stopDrawing();
}
let dir = GLib.build_filenamev([GLib.get_user_data_dir(), DATA_SUB_DIR]);
if (!GLib.file_test(dir, GLib.FileTest.EXISTS))
GLib.mkdir_with_parents(dir, 0o700);
let path = GLib.build_filenamev([dir, `${name}.json`]);
let oldContents;
if (GLib.file_test(path, GLib.FileTest.EXISTS)) {
@ -652,14 +695,30 @@ var DrawingArea = new Lang.Class({
// because of compromise between disk usage and human readability
let contents = `[\n ` + new Array(...this.elements.map(element => JSON.stringify(element))).join(`,\n\n `) + `\n]`;
if (contents != oldContents)
if (contents != oldContents) {
GLib.file_set_contents(path, contents);
if (notify)
this.emit('show-osd', 'document-save-symbolic', name, -1);
if (name != DEFAULT_FILE_NAME)
this.jsonName = name;
}
},
_loadJson: function() {
let filename = `${DEFAULT_FILE_NAME}.json`;
saveAsJsonWithName: function(name) {
this._saveAsJson(name);
},
saveAsJson: function() {
this._saveAsJson(getDateString(), true);
},
savePersistent: function() {
this._saveAsJson(DEFAULT_FILE_NAME);
},
_loadJson: function(name, notify) {
let dir = GLib.get_user_data_dir();
let path = GLib.build_filenamev([dir, filename]);
let path = GLib.build_filenamev([dir, DATA_SUB_DIR, `${name}.json`]);
if (!GLib.file_test(path, GLib.FileTest.EXISTS))
return;
@ -669,6 +728,43 @@ var DrawingArea = new Lang.Class({
if (contents instanceof Uint8Array)
contents = imports.byteArray.toString(contents);
this.elements.push(...JSON.parse(contents).map(object => new DrawingElement(object)));
if (notify)
this.emit('show-osd', 'document-open-symbolic', name, -1);
if (name != DEFAULT_FILE_NAME)
this.jsonName = name;
},
_loadPersistent: function() {
this._loadJson(DEFAULT_FILE_NAME);
},
loadJson: function(name, notify) {
this.elements = [];
this.currentElement = null;
this._stopCursorTimeout();
this._loadJson(name, notify);
this._redisplay();
},
loadNextJson: function() {
let names = getJsonFiles().map(file => file.name);
if (!names.length)
return;
let nextName = names[this.jsonName && names.indexOf(this.jsonName) != names.length - 1 ? names.indexOf(this.jsonName) + 1 : 0];
this.loadJson(nextName, true);
},
loadPreviousJson: function() {
let names = getJsonFiles().map(file => file.name);
if (!names.length)
return;
let previousName = names[this.jsonName && names.indexOf(this.jsonName) > 0 ? names.indexOf(this.jsonName) - 1 : names.length - 1];
this.loadJson(previousName, true);
},
disable: function() {
@ -1144,8 +1240,12 @@ var DrawingMenu = new Lang.Class({
this._addSwitchItemWithCallback(this.menu, _("Square drawing area"), this.area.isSquareArea, this.area.toggleSquareArea.bind(this.area));
this._addSeparator(this.menu);
this.menu.addAction(_("Save drawing as a SVG file"), this.area.saveAsSvg.bind(this.area), 'document-save-symbolic');
this.menu.addAction(_("Open stylesheet.css"), manager.openStylesheetFile.bind(manager), 'document-open-symbolic');
this._addDrawingNameItem(this.menu);
this._addOpenDrawingSubMenuItem(this.menu);
this._addSaveDrawingSubMenuItem(this.menu);
this.menu.addAction(_("Save drawing as a SVG file"), this.area.saveAsSvg.bind(this.area), 'image-x-generic-symbolic');
this.menu.addAction(_("Open stylesheet.css"), manager.openStylesheetFile.bind(manager), 'document-page-setup-symbolic');
this.menu.addAction(_("Show help"), this.area.toggleHelp.bind(this.area), 'preferences-desktop-keyboard-shortcuts-symbolic');
this.updateSectionVisibility();
@ -1277,6 +1377,105 @@ var DrawingMenu = new Lang.Class({
menu.addMenuItem(item);
},
_addDrawingNameItem: function(menu) {
this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false });
this.drawingNameMenuItem.setSensitive(false);
menu.addMenuItem(this.drawingNameMenuItem);
this._updateDrawingNameMenuItem();
},
_updateDrawingNameMenuItem: function() {
getActor(this.drawingNameMenuItem).visible = this.area.jsonName ? true : false;
if (this.area.jsonName) {
this.drawingNameMenuItem.label.set_text(`<i>${this.area.jsonName}</i>`);
this.drawingNameMenuItem.label.get_clutter_text().set_use_markup(true);
}
},
_addOpenDrawingSubMenuItem: function(menu) {
let item = new PopupMenu.PopupSubMenuMenuItem(_("Open drawing"), true);
this.openDrawingSubMenuItem = item;
this.openDrawingSubMenu = item.menu;
item.icon.set_icon_name('document-open-symbolic');
item.menu.itemActivated = () => {
item.menu.close();
};
Mainloop.timeout_add(0, () => {
this._populateOpenDrawingSubMenu();
return GLib.SOURCE_REMOVE;
});
menu.addMenuItem(item);
},
_populateOpenDrawingSubMenu: function() {
this.openDrawingSubMenu.removeAll();
let jsonFiles = getJsonFiles();
jsonFiles.forEach(file => {
let item = this.openDrawingSubMenu.addAction(`<span font_family="Monospace"><i>${file.displayName}</i></span>`, () => {
this.area.loadJson(file.name);
this._updateDrawingNameMenuItem();
this._updateSaveDrawingSubMenuItemSensitivity();
});
item.label.get_clutter_text().set_use_markup(true);
let expander = new St.Bin({
style_class: 'popup-menu-item-expander',
x_expand: true,
});
getActor(item).add_child(expander);
let deleteButton = new St.Button({ style_class: 'draw-on-your-screen-menu-delete-button',
child: new St.Icon({ icon_name: 'edit-delete-symbolic',
style_class: 'popup-menu-icon',
x_align: Clutter.ActorAlign.END }) });
getActor(item).add_child(deleteButton);
deleteButton.connect('clicked', () => {
file.delete();
this._populateOpenDrawingSubMenu();
});
});
this.openDrawingSubMenuItem.setSensitive(!this.openDrawingSubMenu.isEmpty());
},
_addSaveDrawingSubMenuItem: function(menu) {
let item = new PopupMenu.PopupSubMenuMenuItem(_("Save drawing"), true);
this.saveDrawingSubMenuItem = item;
this._updateSaveDrawingSubMenuItemSensitivity();
this.saveDrawingSubMenu = item.menu;
item.icon.set_icon_name('document-save-symbolic');
item.menu.itemActivated = () => {
item.menu.close();
};
Mainloop.timeout_add(0, () => {
this._populateSaveDrawingSubMenu();
return GLib.SOURCE_REMOVE;
});
menu.addMenuItem(item);
},
_updateSaveDrawingSubMenuItemSensitivity: function() {
this.saveDrawingSubMenuItem.setSensitive(this.area.elements.length > 0);
},
_populateSaveDrawingSubMenu: function() {
this.saveEntry = new DrawingMenuEntry({ initialTextGetter: getDateString,
entryActivateCallback: (text) => {
this.area.saveAsJsonWithName(text);
this.saveDrawingSubMenu.toggle();
this._updateDrawingNameMenuItem();
this._populateOpenDrawingSubMenu();
},
invalidStrings: [DEFAULT_FILE_NAME, '/'],
primaryIconName: 'insert-text' });
this.saveDrawingSubMenu.addMenuItem(this.saveEntry.item);
},
_addSeparator: function(menu) {
let separatorItem = new PopupMenu.PopupSeparatorMenuItem(' ');
getActor(separatorItem).add_style_class_name('draw-on-your-screen-menu-separator-item');
@ -1284,3 +1483,87 @@ var DrawingMenu = new Lang.Class({
}
});
// based on searchItem.js, https://github.com/leonardo-bartoli/gnome-shell-extension-Recents
var DrawingMenuEntry = new Lang.Class({
Name: 'DrawOnYourScreenDrawingMenuEntry',
_init: function (params) {
this.params = params;
this.item = new PopupMenu.PopupBaseMenuItem({ style_class: 'draw-on-your-screen-menu-entry-item',
activate: false,
reactive: true,
can_focus: false });
this.itemActor = GS_VERSION < '3.33.0' ? this.item.actor : this.item;
this.entry = new St.Entry({
style_class: 'search-entry draw-on-your-screen-menu-entry',
track_hover: true,
reactive: true,
can_focus: true
});
this.entry.set_primary_icon(new St.Icon({ style_class: 'search-entry-icon',
icon_name: this.params.primaryIconName }));
this.entry.clutter_text.connect('text-changed', this._onTextChanged.bind(this));
this.entry.clutter_text.connect('activate', this._onTextActivated.bind(this));
this.clearIcon = new St.Icon({
style_class: 'search-entry-icon',
icon_name: 'edit-clear-symbolic'
});
this.entry.connect('secondary-icon-clicked', this._reset.bind(this));
getActor(this.item).add(this.entry, { expand: true });
getActor(this.item).connect('notify::mapped', (actor) => {
if (actor.mapped) {
this.entry.set_text(this.params.initialTextGetter());
this.entry.clutter_text.grab_key_focus();
}
});
},
_setError: function(hasError) {
if (hasError)
this.entry.add_style_class_name('draw-on-your-screen-menu-entry-error');
else
this.entry.remove_style_class_name('draw-on-your-screen-menu-entry-error');
},
_reset: function() {
this.entry.text = '';
this.entry.clutter_text.set_cursor_visible(true);
this.entry.clutter_text.set_selection(0, 0);
this._setError(false);
},
_onTextActivated: function(clutterText) {
let text = clutterText.get_text();
if (text.length == 0)
return;
if (this._getIsInvalid())
return;
this._reset();
this.params.entryActivateCallback(text);
},
_onTextChanged: function(clutterText) {
let text = clutterText.get_text();
this.entry.set_secondary_icon(text.length ? this.clearIcon : null);
if (text.length)
this._setError(this._getIsInvalid());
},
_getIsInvalid: function() {
for (let i = 0; i < this.params.invalidStrings.length; i++) {
if (this.entry.text.indexOf(this.params.invalidStrings[i]) != -1)
return true;
}
return false;
}
});

View File

@ -113,7 +113,7 @@ var AreaManager = new Lang.Class({
onPersistentSettingChanged: function() {
if (this.settings.get_boolean('persistent-drawing'))
this.areas[Main.layoutManager.primaryIndex].saveAsJson();
this.areas[Main.layoutManager.primaryIndex].savePersistent();
},
updateIndicator: function() {
@ -136,8 +136,8 @@ var AreaManager = new Lang.Class({
let monitor = this.monitors[i];
let container = new St.Widget({ name: 'drawOnYourSreenContainer' + i });
let helper = new Draw.DrawingHelper({ name: 'drawOnYourSreenHelper' + i }, monitor);
let load = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing');
let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, load);
let loadPersistent = i == Main.layoutManager.primaryIndex && this.settings.get_boolean('persistent-drawing');
let area = new Draw.DrawingArea({ name: 'drawOnYourSreenArea' + i }, monitor, helper, loadPersistent);
container.add_child(area);
container.add_child(helper);
@ -161,6 +161,9 @@ var AreaManager = new Lang.Class({
'delete-last-element': this.activeArea.deleteLastElement.bind(this.activeArea),
'smooth-last-element': this.activeArea.smoothLastElement.bind(this.activeArea),
'save-as-svg': this.activeArea.saveAsSvg.bind(this.activeArea),
'save-as-json': this.activeArea.saveAsJson.bind(this.activeArea),
'open-previous-json': this.activeArea.loadPreviousJson.bind(this.activeArea),
'open-next-json': this.activeArea.loadNextJson.bind(this.activeArea),
'toggle-background': this.activeArea.toggleBackground.bind(this.activeArea),
'toggle-square-area': this.activeArea.toggleSquareArea.bind(this.activeArea),
'increment-line-width': () => this.activeArea.incrementLineWidth(1),
@ -223,7 +226,7 @@ var AreaManager = new Lang.Class({
for (let i = 0; i < this.areas.length; i++)
this.areas[i].erase();
if (this.settings.get_boolean('persistent-drawing'))
this.areas[Main.layoutManager.primaryIndex].saveAsJson();
this.areas[Main.layoutManager.primaryIndex].savePersistent();
},
togglePanelAndDockOpacity: function() {

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Draw On Your Screen VERSION\n"
"Report-Msgid-Bugs-To: https://framagit.org/abakkk/DrawOnYourScreen/issues\n"
"POT-Creation-Date: 2019-03-04 16:40+0100\n"
"POT-Creation-Date: 2020-01-03 08:00+0100\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"
@ -108,6 +108,12 @@ msgstr ""
msgid "Color"
msgstr ""
msgid "Open drawing"
msgstr ""
msgid "Save drawing"
msgstr ""
#: prefs.js
msgid "Preferences"
@ -196,6 +202,16 @@ msgstr ""
msgid "Square drawing area"
msgstr ""
msgid "Open previous drawing"
msgstr ""
msgid "Open next drawing"
msgstr ""
# already in draw.js
#msgid "Save drawing"
#msgstr ""
msgid "Save drawing as a SVG file"
msgstr ""

View File

@ -68,6 +68,9 @@ var INTERNAL_KEYBINDINGS = {
'toggle-background': "Add a drawing background",
'toggle-square-area': "Square drawing area",
'-separator-5': '',
'open-previous-json': "Open previous drawing",
'open-next-json': "Open next drawing",
'save-as-json': "Save drawing",
'save-as-svg': "Save drawing as a SVG file",
'open-stylesheet': "Open stylesheet.css",
'toggle-help': "Show help"

Binary file not shown.

View File

@ -206,6 +206,21 @@
<summary>Save drawing as a svg file</summary>
<description>Save drawing as a svg file</description>
</key>
<key type="as" name="save-as-json">
<default>["&lt;Primary&gt;&lt;Shift&gt;s"]</default>
<summary>Save drawing as a json file</summary>
<description>Save drawing as a json file</description>
</key>
<key type="as" name="open-previous-json">
<default>["&lt;Primary&gt;Left"]</default>
<summary>Open previous json file</summary>
<description>Open previous json file</description>
</key>
<key type="as" name="open-next-json">
<default>["&lt;Primary&gt;Right"]</default>
<summary>Open next json file</summary>
<description>Open next json file</description>
</key>
<key type="as" name="toggle-help">
<default>["&lt;Primary&gt;F1"]</default>
<summary>toggle help</summary>

View File

@ -115,5 +115,29 @@
min-width: 3em;
text-align: right;
}
.draw-on-your-screen-menu-entry-item {
padding-right: 1.15em; /* default 1.75em */
spacing: 0; /* default 12px */
}
.draw-on-your-screen-menu-entry {
border: none;
border-radius: 3px;
padding: 0.35em 0.57em;
width: 10em;
}
.draw-on-your-screen-menu-entry-error {
color: #f57900; /* upstream warning_color */
}
.draw-on-your-screen-menu-entry:focus {
padding: 0.35em 0.57em;
}
.draw-on-your-screen-menu-delete-button:hover {
color: #f57900;
}