Skip to content

Commit f693326

Browse files
authored
quicklinks@NotSirius-A: Version 1.3 (#1643)
1 parent 564d139 commit f693326

File tree

8 files changed

+453
-132
lines changed

8 files changed

+453
-132
lines changed

quicklinks@NotSirius-A/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ You can customize this desklet to your taste. Choose from two layouts a list or
2525
- You can add a transparent border to your link to make them look bigger.
2626
- When you disable desklet decorations you can still grab the desklet by the invisible border around it.
2727

28-
### What can be improved?
29-
30-
* Improve font parsing.
31-
32-
* Unfortunately, the xlet settings API at this moment doesn't support `iconfilechooser` within `list` setting, so icons have to be typed in by name, which is somewhat annoying.
33-
3428

3529
### Remarks
3630

quicklinks@NotSirius-A/files/quicklinks@NotSirius-A/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## [1.3] - 09.11.2025
4+
5+
### Added
6+
- PopupMenuItem: "Add new link", which opens a gtk dialog, when confirmed it adds a new link
7+
- file `open_add_edit_dialog_gtk.py`
8+
9+
### Changed
10+
- Improved font parsing
11+
- Changed desklet name from "Quick Links" to "Quick Links - Launcher" to make it clearer what this desklet does
12+
- Changed desklet description: added the word "launcher"
13+
- Updated `[email protected]`
14+
- Some default visual settings
15+
- Improved border rendering
16+
- Minor setting changes
17+
- Changed "icon-name" setting from string to icon. Why did no one tell me there's an icon data type inside lists!!
18+
- Updated `README.md`
19+
20+
### Removed
21+
22+
- Bold/italic options, because they are no longer needed
323

424
## [1.2] - 14.10.2025
525

quicklinks@NotSirius-A/files/quicklinks@NotSirius-A/desklet.js

Lines changed: 166 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Gio = imports.gi.Gio;
1212
const Gettext = imports.gettext;
1313
const Pango = imports.gi.Pango;
1414
const Tooltips = imports.ui.tooltips;
15+
const PopupMenu = imports.ui.popupMenu;
1516

1617
const UUID = "quicklinks@NotSirius-A";
1718
const DESKLET_ROOT = imports.ui.deskletManager.deskletMeta[UUID].path;
@@ -57,8 +58,6 @@ MyDesklet.prototype = {
5758
this.settings.bindProperty(Settings.BindingDirection.IN, "link-border-color", "linkBorderColor", this.on_setting_changed);
5859
this.settings.bindProperty(Settings.BindingDirection.IN, "icon-size", "iconSize", this.on_setting_changed);
5960
this.settings.bindProperty(Settings.BindingDirection.IN, "font", "fontRaw", this.on_setting_changed);
60-
this.settings.bindProperty(Settings.BindingDirection.IN, "font-bold", "fontBold", this.on_setting_changed);
61-
this.settings.bindProperty(Settings.BindingDirection.IN, "font-italic", "fontItalic", this.on_setting_changed);
6261
this.settings.bindProperty(Settings.BindingDirection.IN, "scale-size", "scaleSize", this.on_setting_changed);
6362
this.settings.bindProperty(Settings.BindingDirection.IN, "font-enabled", "fontEnabled", this.on_setting_changed);
6463
this.settings.bindProperty(Settings.BindingDirection.IN, "text-color", "customTextColor", this.on_setting_changed);
@@ -82,42 +81,96 @@ MyDesklet.prototype = {
8281
* Starts desklet, imports links, renders UI
8382
*/
8483
runDesklet: function() {
84+
this.initializeData();
8585

86-
this.lastClick = {
87-
"link_index": null,
88-
"time": Infinity,
89-
};
90-
91-
// MAKE SURE to create a deepcopy of linkList, otherwise you could modify actual desklet settings using this variable
92-
let links_list_deepcopy = JSON.parse(JSON.stringify(this.linksList));
93-
this.links = this.getLinks(links_list_deepcopy);
94-
95-
this.font = this.parseFont(this.fontRaw);
9686
// Render desklet gui
9787
this.renderGUI();
9888

89+
this.populateContextMenu();
90+
9991
// Render system desklet decorations
10092
this.renderDecorations();
10193
},
10294

103-
/**
104-
* Parse raw font string, TODO improve parsing, detect bold/italic etc.
105-
* @param {string} fontString - Font descriptor
106-
* @returns {{"family": string, "size": Number}} Font descriptor object
107-
*/
108-
parseFont: function(fontString) {
109-
fontString = fontString.trim();
110-
const font_split = fontString.split(" ");
111-
112-
const font_size = parseInt(font_split.pop());
113-
let font_family = font_split.join(" ");
114-
115-
return {
116-
"family": font_family,
117-
"size": font_size
95+
initializeData: function() {
96+
this.lastClick = {
97+
"link_index": null,
98+
"time": Infinity,
11899
};
100+
101+
// MAKE SURE to create a deepcopy of linkList, otherwise you could modify actual desklet settings using this variable
102+
this.links_list_deepcopy = JSON.parse(JSON.stringify(this.linksList));
103+
this.links = this.getLinks(this.links_list_deepcopy);
104+
105+
this.font = this.parseFontStringToCSS(this.fontRaw);
119106
},
120107

108+
/**
109+
* Parse raw font string.
110+
* @param {string} font_string - Font descriptor string
111+
* @returns {{"font-family": string, "font-size": Number, "font-weight": Number, "font-style": string, "font-stretch": string}} Font descriptor object
112+
*/
113+
parseFontStringToCSS: function(font_string) {
114+
// Some fonts don't work, so a fallback font is a good idea
115+
const fallback_font_str = "Ubuntu Regular 16";
116+
117+
// String are passed by reference here
118+
// make sure to copy the string to avoid triggering settings callback on change
119+
const font_string_copy = font_string.slice().trim();
120+
121+
let css_font;
122+
try {
123+
const my_font_description = Pango.font_description_from_string(font_string_copy);
124+
css_font = this._PangoFontDescriptionToCSS(my_font_description);
125+
} catch (e) {
126+
Main.notifyError(
127+
_("Sorry, this font is not supported, please select a different one.")
128+
+ _(" Font: `") + font_string_copy + _("` Error: ")
129+
+ e.toString()
130+
);
131+
132+
const fallback_font_description = Pango.font_description_from_string(fallback_font_str);
133+
css_font = this._PangoFontDescriptionToCSS(fallback_font_description);
134+
} finally {
135+
return css_font;
136+
}
137+
138+
},
139+
140+
141+
/**
142+
* Process Pango.FontDescription and return valid CSS values
143+
* @param {Pango.FontDescription} font_description - Font descriptor
144+
* @returns {{"font-family": string, "font-size": Number, "font-weight": Number, "font-style": string, "font-stretch": string}} Font descriptor object
145+
*/
146+
_PangoFontDescriptionToCSS: function(font_description) {
147+
const PangoStyle_to_CSS_map = {
148+
[Pango.Style.NORMAL]: "normal",
149+
[Pango.Style.OBLIQUE]: "oblique",
150+
[Pango.Style.ITALIC]: "italic",
151+
};
152+
153+
// font-stretch CSS property seems to be ignored by the CSS renderer
154+
const PangoStretch_to_CSS_map = {
155+
[Pango.Stretch.ULTRA_CONDENSED]: "ultra-condensed",
156+
[Pango.Stretch.EXTRA_CONDENSED]: "extra-condensed",
157+
[Pango.Stretch.CONDENSED]: "condensed",
158+
[Pango.Stretch.NORMAL]: "normal",
159+
[Pango.Stretch.SEMI_EXPANDED]: "semi-expanded",
160+
[Pango.Stretch.EXPANDED]: "expanded",
161+
[Pango.Stretch.EXTRA_EXPANDED]: "extra-expanded",
162+
[Pango.Stretch.ULTRA_EXPANDED]: "ultra-expanded",
163+
};
164+
165+
return {
166+
"font-family": font_description.get_family(),
167+
"font-size": Math.floor(font_description.get_size() / Pango.SCALE),
168+
"font-weight": font_description.get_weight(),
169+
"font-style": PangoStyle_to_CSS_map[font_description.get_style()],
170+
"font-stretch": PangoStretch_to_CSS_map[font_description.get_stretch()]
171+
};
172+
},
173+
121174

122175
/**
123176
* Process raw links and output links that are ready to be rendered onto the screen
@@ -161,16 +214,17 @@ MyDesklet.prototype = {
161214
const desktop_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.interface" });
162215
this.text_scale = desktop_settings.get_double("text-scaling-factor");
163216

164-
this.font["size"] = this.font["size"] * this.text_scale;
217+
this.font["font-size"] = this.font["font-size"] * this.text_scale;
165218

166-
// For some reason the border is inside the element not outside like normal CSS?? So border needs to be taken into account here
167-
const default_link_width = 80 + this.linkBorderWidth*12;
219+
const default_link_width = 80;
168220

169221
// Calculate new sizes based on scale and layout
170222
// Multipliers are based on experimentation with what looks good
171223
const width_scale = (this.deskletLayout === "tile")? 0.7 : 1
172224
this.scale = this.scaleSize * global.ui_scale * width_scale;
173-
this.link_width = default_link_width * this.scale * this.text_scale;
225+
226+
// For some reason the border is inside the element not outside like normal CSS?? So border needs to be taken into account here
227+
this.link_width = default_link_width * this.scale * this.text_scale + this.linkBorderWidth*2;
174228

175229
const spacing_scale = (this.deskletLayout === "tile")? 2 : 1;
176230
const link_row_spacing = this.rowSpacing * this.scale * spacing_scale;
@@ -222,12 +276,13 @@ MyDesklet.prototype = {
222276
link_el.set_width(Math.round(this.link_width));
223277

224278

225-
link_el.style = "font-family: '" + this.font["family"] + "';"
226-
+ "font-size: " + this.font["size"] + "px;"
279+
link_el.style = "font-family: '" + this.font["font-family"] + "';"
280+
+ "font-size: " + this.font["font-size"] + "px;"
227281
+ "color:" + this.customTextColor + ";"
228-
+ "font-weight:" + (this.fontBold ? "bold" : "normal") + ";"
229-
+ "font-style:" + (this.fontItalic ? "italic" : "normal") + ";"
230-
+ "text-shadow:" + (this.textShadow ? "1px 1px 6px "+this.textShadowColor : "none") + ";"
282+
+ "font-weight:" + this.font["font-weight"] + ";"
283+
+ "font-style:" + this.font["font-style"] + ";"
284+
+ "font-stretch:" + this.font["font-stretch"] + ";"
285+
+ "text-shadow:" + (this.textShadow ? "2px 2px 4px "+this.textShadowColor : "none") + ";"
231286
+ "background-color:" + (this.isTransparentBg ? "unset" : this.customBgColor) + ";"
232287
+ "text-align:" + this.textAlign + ";"
233288
+ "border: solid " + this.linkBorderWidth + "px " + this.linkBorderColor + ";";
@@ -265,7 +320,7 @@ MyDesklet.prototype = {
265320
if (this.deskletLayout === "tile") {
266321
label_width = Math.round(this.link_width * 0.92);
267322
} else {
268-
label_width = Math.round(this.link_width - global.ui_scale * (this.iconSize*1.1 + this.font["size"]*1.15 + 5))
323+
label_width = Math.round(this.link_width - global.ui_scale * (this.iconSize*1.1 + this.font["font-size"]*1.15 + 5) - this.linkBorderWidth*1.1)
269324
label_width = Math.max(label_width, 0);
270325
}
271326
link_label.set_width(label_width);
@@ -369,6 +424,75 @@ MyDesklet.prototype = {
369424

370425
},
371426

427+
/**
428+
* Add options to context menu
429+
*/
430+
populateContextMenu: function() {
431+
let menuItem = new PopupMenu.PopupMenuItem(_("Add new link"));
432+
this._menu.addMenuItem(menuItem);
433+
menuItem.connect("activate", Lang.bind(this, Lang.bind(this, () => {
434+
this.handleAddEditDialog();
435+
})));
436+
},
437+
438+
handleAddEditDialog() {
439+
try {
440+
441+
// Column ids here must match columns ids in the settings
442+
const columns = [
443+
{"id": "is-visible", "title": "Display", "type": "boolean", "default": true},
444+
{"id": "name", "title": "Name", "type": "string", "default": "Name"},
445+
{"id": "icon-name", "title": "Icon", "type": "icon", "default": "folder"},
446+
{"id": "command", "title": "Command", "type": "string", "default": "nemo /"},
447+
{"id": "shell", "title": "Shell", "type": "boolean", "default": true, "align": 0}
448+
];
449+
450+
const [success_, command_argv] = GLib.shell_parse_argv(
451+
`python3 ${DESKLET_ROOT}/open_add_edit_dialog_gtk.py '${JSON.stringify(columns).replaceAll("'", "")}'`
452+
);
453+
454+
455+
const proc = Gio.Subprocess.new(
456+
command_argv,
457+
Gio.SubprocessFlags.STDOUT_PIPE
458+
);
459+
460+
// Python script prints the output of the dialog when user accepts it, code here is retrieving this output from the stdout pipe
461+
proc.wait_async(null, () => {
462+
proc.communicate_utf8_async(null, null, (proc, result) => {
463+
464+
const [is_ok, dialog_output, _] = proc.communicate_utf8_finish(result);
465+
let dialog_json;
466+
try {
467+
dialog_json = JSON.parse(dialog_output);
468+
} catch(e) {
469+
global.logError(e);
470+
return;
471+
}
472+
473+
// Add a new link when communication was correct and response doesn't have error:true
474+
if (is_ok && (!dialog_json.error || dialog_json.error === undefined)) {
475+
476+
try {
477+
let new_settings_list = this.links_list_deepcopy.concat(JSON.parse(dialog_output));
478+
this.settings.setValue("links-list", new_settings_list);
479+
this.initializeData();
480+
this.renderGUI();
481+
} catch (e) {
482+
global.logError(e);
483+
return;
484+
}
485+
486+
}
487+
});
488+
});
489+
490+
} catch (e) {
491+
global.logError(e);
492+
}
493+
},
494+
495+
372496
/**
373497
* Render desklet decorations
374498
*/
@@ -384,15 +508,18 @@ MyDesklet.prototype = {
384508
* This function should be used as a callback when settings change
385509
*/
386510
on_setting_changed: function() {
387-
this.runDesklet();
511+
this.initializeData();
512+
this.renderGUI();
513+
this.renderDecorations();
388514
},
389515

390516

391517
/**
392518
* This function should be used as a callback user clicks a button in the settings
393519
*/
394520
on_reset_links_callback: function() {
395-
this.runDesklet();
521+
this.initializeData();
522+
this.renderGUI();
396523
}
397524

398525
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"max-instances": "10",
3-
"description": "Customizable quick links panel. Allows you to create beautiful themed desktop shortcuts for your most used folders/commands.",
4-
"name": "Quick Links",
5-
"version": "1.2",
3+
"description": "Customizable quick links launcher panel. Allows you to create beautiful themed desktop shortcuts for your most used folders/commands.",
4+
"name": "Quick Links - Launcher",
5+
"version": "1.3",
66
"uuid": "quicklinks@NotSirius-A",
77
"author": "NotSirius-A"
88
}

0 commit comments

Comments
 (0)