WebUI: Filter list improvements
[qBittorrent.git] / src / webui / www / private / scripts / contextmenu.js
blob18bccbff904e3a7f3a414bcbe49f3d8f98c200a3
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2009 Christophe Dumez <chris@qbittorrent.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License
7 * as published by the Free Software Foundation; either version 2
8 * of the License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * In addition, as a special exception, the copyright holders give permission to
20 * link this program with the OpenSSL project's "OpenSSL" library (or with
21 * modified versions of it that use the same license as the "OpenSSL" library),
22 * and distribute the linked executables. You must obey the GNU General Public
23 * License in all respects for all of the code used other than "OpenSSL". If you
24 * modify file(s), you may extend this exception to your version of the file(s),
25 * but you are not obligated to do so. If you do not wish to do so, delete this
26 * exception statement from your version.
29 "use strict";
31 window.qBittorrent ??= {};
32 window.qBittorrent.ContextMenu ??= (() => {
33 const exports = () => {
34 return {
35 ContextMenu: ContextMenu,
36 TorrentsTableContextMenu: TorrentsTableContextMenu,
37 CategoriesFilterContextMenu: CategoriesFilterContextMenu,
38 TagsFilterContextMenu: TagsFilterContextMenu,
39 SearchPluginsTableContextMenu: SearchPluginsTableContextMenu,
40 RssFeedContextMenu: RssFeedContextMenu,
41 RssArticleContextMenu: RssArticleContextMenu,
42 RssDownloaderRuleContextMenu: RssDownloaderRuleContextMenu
46 let lastShownContextMenu = null;
47 const ContextMenu = new Class({
48 // implements
49 Implements: [Options, Events],
51 // options
52 options: {
53 actions: {},
54 menu: "menu_id",
55 stopEvent: true,
56 targets: "body",
57 offsets: {
58 x: 0,
59 y: 0
61 onShow: () => {},
62 onHide: () => {},
63 onClick: () => {},
64 fadeSpeed: 200,
65 touchTimer: 600
68 // initialization
69 initialize: function(options) {
70 // set options
71 this.setOptions(options);
73 // option diffs menu
74 this.menu = $(this.options.menu);
75 this.targets = $$(this.options.targets);
77 // fx
78 this.fx = new Fx.Tween(this.menu, {
79 property: "opacity",
80 duration: this.options.fadeSpeed,
81 onComplete: () => {
82 this.menu.style.visibility = (getComputedStyle(this.menu).opacity > 0) ? "visible" : "hidden";
84 });
86 // hide and begin the listener
87 this.hide().startListener();
89 // hide the menu
90 this.menu.style.position = "absolute";
91 this.menu.style.top = "-900000px";
92 this.menu.style.display = "block";
95 adjustMenuPosition: function(e) {
96 this.updateMenuItems();
98 const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75;
100 if (this.menu.hasClass("scrollableMenu"))
101 this.menu.style.maxHeight = `${scrollableMenuMaxHeight}px`;
103 // draw the menu off-screen to know the menu dimensions
104 this.menu.style.left = "-999em";
105 this.menu.style.top = "-999em";
107 // position the menu
108 let xPosMenu = e.pageX + this.options.offsets.x;
109 let yPosMenu = e.pageY + this.options.offsets.y;
110 if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
111 xPosMenu -= this.menu.offsetWidth;
112 if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
113 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
114 if (xPosMenu < 0)
115 xPosMenu = 0;
116 if (yPosMenu < 0)
117 yPosMenu = 0;
118 this.menu.style.left = `${xPosMenu}px`;
119 this.menu.style.top = `${yPosMenu}px`;
120 this.menu.style.position = "absolute";
121 this.menu.style.zIndex = "2000";
123 // position the sub-menu
124 const uls = this.menu.getElementsByTagName("ul");
125 for (let i = 0; i < uls.length; ++i) {
126 const ul = uls[i];
127 if (ul.hasClass("scrollableMenu"))
128 ul.style.maxHeight = `${scrollableMenuMaxHeight}px`;
129 const rectParent = ul.parentNode.getBoundingClientRect();
130 const xPosOrigin = rectParent.left;
131 const yPosOrigin = rectParent.bottom;
132 let xPos = xPosOrigin + rectParent.width - 1;
133 let yPos = yPosOrigin - rectParent.height - 1;
134 if ((xPos + ul.offsetWidth) > document.documentElement.clientWidth)
135 xPos -= (ul.offsetWidth + rectParent.width - 2);
136 if ((yPos + ul.offsetHeight) > document.documentElement.clientHeight)
137 yPos = document.documentElement.clientHeight - ul.offsetHeight;
138 if (xPos < 0)
139 xPos = 0;
140 if (yPos < 0)
141 yPos = 0;
142 ul.style.marginLeft = `${xPos - xPosOrigin}px`;
143 ul.style.marginTop = `${yPos - yPosOrigin}px`;
147 setupEventListeners: function(elem) {
148 elem.addEventListener("contextmenu", (e) => {
149 this.triggerMenu(e, elem);
151 elem.addEventListener("click", (e) => {
152 this.hide();
155 elem.addEventListener("touchstart", (e) => {
156 this.hide();
157 this.touchStartAt = performance.now();
158 this.touchStartEvent = e;
159 }, { passive: true });
160 elem.addEventListener("touchend", (e) => {
161 const now = performance.now();
162 const touchStartAt = this.touchStartAt;
163 const touchStartEvent = this.touchStartEvent;
165 this.touchStartAt = null;
166 this.touchStartEvent = null;
168 const isTargetUnchanged = (Math.abs(e.event.pageX - touchStartEvent.event.pageX) <= 10) && (Math.abs(e.event.pageY - touchStartEvent.event.pageY) <= 10);
169 if (((now - touchStartAt) >= this.options.touchTimer) && isTargetUnchanged)
170 this.triggerMenu(touchStartEvent, elem);
171 }, { passive: true });
174 addTarget: function(t) {
175 // prevent long press from selecting this text
176 t.style.userSelect = "none";
178 this.targets[this.targets.length] = t;
179 this.setupEventListeners(t);
182 searchAndAddTargets: function() {
183 document.querySelectorAll(this.options.targets).forEach((target) => { this.addTarget(target); });
186 triggerMenu: function(e, el) {
187 if (this.options.disabled)
188 return;
190 // prevent default, if told to
191 if (this.options.stopEvent) {
192 e.preventDefault();
193 e.stopPropagation();
195 // record this as the trigger
196 this.options.element = $(el);
197 this.adjustMenuPosition(e);
198 // show the menu
199 this.show();
202 // get things started
203 startListener: function() {
204 /* all elements */
205 this.targets.each((el) => {
206 this.setupEventListeners(el);
207 }, this);
209 /* menu items */
210 this.menu.getElements("a").each(function(item) {
211 item.addEventListener("click", (e) => {
212 e.preventDefault();
213 if (!item.hasClass("disabled")) {
214 this.execute(item.href.split("#")[1], $(this.options.element));
215 this.fireEvent("click", [item, e]);
218 }, this);
220 // hide on body click
221 $(document.body).addEventListener("click", () => {
222 this.hide();
226 updateMenuItems: function() {},
228 // show menu
229 show: function(trigger) {
230 if (lastShownContextMenu && (lastShownContextMenu !== this))
231 lastShownContextMenu.hide();
232 this.fx.start(1);
233 this.fireEvent("show");
234 lastShownContextMenu = this;
235 return this;
238 // hide the menu
239 hide: function(trigger) {
240 if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
241 this.fx.start(0);
242 // this.menu.fade('out');
243 this.fireEvent("hide");
245 return this;
248 setItemChecked: function(item, checked) {
249 this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity =
250 checked ? "1" : "0";
251 return this;
254 getItemChecked: function(item) {
255 return this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity !== "0";
258 // hide an item
259 hideItem: function(item) {
260 this.menu.getElement("a[href$=" + item + "]").parentNode.addClass("invisible");
261 return this;
264 // show an item
265 showItem: function(item) {
266 this.menu.getElement("a[href$=" + item + "]").parentNode.removeClass("invisible");
267 return this;
270 // disable the entire menu
271 disable: function() {
272 this.options.disabled = true;
273 return this;
276 // enable the entire menu
277 enable: function() {
278 this.options.disabled = false;
279 return this;
282 // execute an action
283 execute: function(action, element) {
284 if (this.options.actions[action])
285 this.options.actions[action](element, this, action);
286 return this;
290 const TorrentsTableContextMenu = new Class({
291 Extends: ContextMenu,
293 updateMenuItems: function() {
294 let all_are_seq_dl = true;
295 let there_are_seq_dl = false;
296 let all_are_f_l_piece_prio = true;
297 let there_are_f_l_piece_prio = false;
298 let all_are_downloaded = true;
299 let all_are_stopped = true;
300 let there_are_stopped = false;
301 let all_are_force_start = true;
302 let there_are_force_start = false;
303 let all_are_super_seeding = true;
304 let all_are_auto_tmm = true;
305 let there_are_auto_tmm = false;
306 const tagCount = new Map();
307 const categoryCount = new Map();
309 const selectedRows = torrentsTable.selectedRowsIds();
310 selectedRows.forEach((item, index) => {
311 const data = torrentsTable.rows.get(item).full_data;
313 if (data["seq_dl"] !== true)
314 all_are_seq_dl = false;
315 else
316 there_are_seq_dl = true;
318 if (data["f_l_piece_prio"] !== true)
319 all_are_f_l_piece_prio = false;
320 else
321 there_are_f_l_piece_prio = true;
323 if (data["progress"] !== 1.0) // not downloaded
324 all_are_downloaded = false;
325 else if (data["super_seeding"] !== true)
326 all_are_super_seeding = false;
328 if ((data["state"] !== "stoppedUP") && (data["state"] !== "stoppedDL"))
329 all_are_stopped = false;
330 else
331 there_are_stopped = true;
333 if (data["force_start"] !== true)
334 all_are_force_start = false;
335 else
336 there_are_force_start = true;
338 if (data["auto_tmm"] === true)
339 there_are_auto_tmm = true;
340 else
341 all_are_auto_tmm = false;
343 const torrentTags = data["tags"].split(", ");
344 for (const tag of torrentTags) {
345 const count = tagCount.get(tag);
346 tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
349 const torrentCategory = data["category"];
350 const count = categoryCount.get(torrentCategory);
351 categoryCount.set(torrentCategory, ((count !== undefined) ? (count + 1) : 1));
354 // hide renameFiles when more than 1 torrent is selected
355 if (selectedRows.length === 1) {
356 const data = torrentsTable.rows.get(selectedRows[0]).full_data;
357 const metadata_downloaded = !((data["state"] === "metaDL") || (data["state"] === "forcedMetaDL") || (data["total_size"] === -1));
359 // hide renameFiles when metadata hasn't been downloaded yet
360 metadata_downloaded
361 ? this.showItem("renameFiles")
362 : this.hideItem("renameFiles");
364 else {
365 this.hideItem("renameFiles");
368 if (all_are_downloaded) {
369 this.hideItem("downloadLimit");
370 this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
371 this.hideItem("sequentialDownload");
372 this.hideItem("firstLastPiecePrio");
373 this.showItem("superSeeding");
374 this.setItemChecked("superSeeding", all_are_super_seeding);
376 else {
377 const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
378 const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
380 if (!show_seq_dl && show_f_l_piece_prio)
381 this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
382 else
383 this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
385 if (show_seq_dl)
386 this.showItem("sequentialDownload");
387 else
388 this.hideItem("sequentialDownload");
390 if (show_f_l_piece_prio)
391 this.showItem("firstLastPiecePrio");
392 else
393 this.hideItem("firstLastPiecePrio");
395 this.setItemChecked("sequentialDownload", all_are_seq_dl);
396 this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
398 this.showItem("downloadLimit");
399 this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
400 this.hideItem("superSeeding");
403 this.showItem("start");
404 this.showItem("stop");
405 this.showItem("forceStart");
406 if (all_are_stopped)
407 this.hideItem("stop");
408 else if (all_are_force_start)
409 this.hideItem("forceStart");
410 else if (!there_are_stopped && !there_are_force_start)
411 this.hideItem("start");
413 if (!all_are_auto_tmm && there_are_auto_tmm) {
414 this.hideItem("autoTorrentManagement");
416 else {
417 this.showItem("autoTorrentManagement");
418 this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
421 const contextTagList = $("contextTagList");
422 tagList.forEach((tag, tagHash) => {
423 const checkbox = contextTagList.getElement(`a[href="#Tag/${tag.name}"] input[type="checkbox"]`);
424 const count = tagCount.get(tag.name);
425 const hasCount = (count !== undefined);
426 const isLesser = (count < selectedRows.length);
427 checkbox.indeterminate = (hasCount ? isLesser : false);
428 checkbox.checked = (hasCount ? !isLesser : false);
431 const contextCategoryList = document.getElementById("contextCategoryList");
432 category_list.forEach((category, categoryHash) => {
433 const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category.name}"] img`);
434 const count = categoryCount.get(category.name);
435 const isEqual = ((count !== undefined) && (count === selectedRows.length));
436 categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual);
440 updateCategoriesSubMenu: function(categoryList) {
441 const contextCategoryList = $("contextCategoryList");
442 contextCategoryList.getChildren().each(c => c.destroy());
444 const createMenuItem = (text, imgURL, clickFn) => {
445 const anchor = document.createElement("a");
446 anchor.textContent = text;
447 anchor.addEventListener("click", () => { clickFn(); });
449 const img = document.createElement("img");
450 img.src = imgURL;
451 img.alt = text;
452 anchor.prepend(img);
454 const item = document.createElement("li");
455 item.appendChild(anchor);
457 return item;
459 contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN));
460 contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(0); }));
462 const sortedCategories = [];
463 categoryList.forEach((category, hash) => sortedCategories.push({
464 categoryName: category.name,
465 categoryHash: hash
466 }));
467 sortedCategories.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(
468 left.categoryName, right.categoryName));
470 let first = true;
471 for (const { categoryName, categoryHash } of sortedCategories) {
472 const anchor = document.createElement("a");
473 anchor.href = `#Category/${categoryName}`;
474 anchor.textContent = categoryName;
475 anchor.addEventListener("click", (event) => {
476 torrentSetCategoryFN(categoryHash);
479 const img = document.createElement("img");
480 img.src = "images/view-categories.svg";
481 anchor.prepend(img);
483 const setCategoryItem = document.createElement("li");
484 setCategoryItem.appendChild(anchor);
485 if (first) {
486 setCategoryItem.addClass("separator");
487 first = false;
490 contextCategoryList.appendChild(setCategoryItem);
494 updateTagsSubMenu: function(tagList) {
495 const contextTagList = $("contextTagList");
496 while (contextTagList.firstChild !== null)
497 contextTagList.removeChild(contextTagList.firstChild);
499 const createMenuItem = (text, imgURL, clickFn) => {
500 const anchor = document.createElement("a");
501 anchor.textContent = text;
502 anchor.addEventListener("click", () => { clickFn(); });
504 const img = document.createElement("img");
505 img.src = imgURL;
506 img.alt = text;
507 anchor.prepend(img);
509 const item = document.createElement("li");
510 item.appendChild(anchor);
512 return item;
514 contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN));
515 contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN));
517 const sortedTags = [];
518 tagList.forEach((tag, hash) => sortedTags.push({
519 tagName: tag.name,
520 tagHash: hash
521 }));
522 sortedTags.sort((left, right) => window.qBittorrent.Misc.naturalSortCollator.compare(left.tagName, right.tagName));
524 for (let i = 0; i < sortedTags.length; ++i) {
525 const { tagName, tagHash } = sortedTags[i];
527 const input = document.createElement("input");
528 input.type = "checkbox";
529 input.addEventListener("click", (event) => {
530 input.checked = !input.checked;
533 const anchor = document.createElement("a");
534 anchor.href = `#Tag/${tagName}`;
535 anchor.textContent = tagName;
536 anchor.addEventListener("click", (event) => {
537 event.preventDefault();
538 torrentSetTagsFN(tagHash, !input.checked);
540 anchor.prepend(input);
542 const setTagItem = document.createElement("li");
543 setTagItem.appendChild(anchor);
544 if (i === 0)
545 setTagItem.addClass("separator");
547 contextTagList.appendChild(setTagItem);
552 const CategoriesFilterContextMenu = new Class({
553 Extends: ContextMenu,
554 updateMenuItems: function() {
555 const id = Number(this.options.element.id);
556 if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
557 this.showItem("editCategory");
558 this.showItem("deleteCategory");
559 if (useSubcategories)
560 this.showItem("createSubcategory");
561 else
562 this.hideItem("createSubcategory");
564 else {
565 this.hideItem("editCategory");
566 this.hideItem("deleteCategory");
567 this.hideItem("createSubcategory");
572 const TagsFilterContextMenu = new Class({
573 Extends: ContextMenu,
574 updateMenuItems: function() {
575 const id = Number(this.options.element.id);
576 if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
577 this.showItem("deleteTag");
578 else
579 this.hideItem("deleteTag");
583 const SearchPluginsTableContextMenu = new Class({
584 Extends: ContextMenu,
586 updateMenuItems: function() {
587 const enabledColumnIndex = function(text) {
588 const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
589 for (let i = 0; i < columns.length; ++i) {
590 if (columns[i].textContent === "Enabled")
591 return i;
595 this.showItem("Enabled");
596 this.setItemChecked("Enabled", (this.options.element.getChildren("td")[enabledColumnIndex()].textContent === "Yes"));
598 this.showItem("Uninstall");
602 const RssFeedContextMenu = new Class({
603 Extends: ContextMenu,
604 updateMenuItems: function() {
605 const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
606 this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
607 switch (selectedRows.length) {
608 case 0:
609 // remove separator on top of newSubscription entry to avoid double line
610 this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
611 // menu when nothing selected
612 this.hideItem("update");
613 this.hideItem("markRead");
614 this.hideItem("rename");
615 this.hideItem("delete");
616 this.showItem("newSubscription");
617 this.showItem("newFolder");
618 this.showItem("updateAll");
619 this.hideItem("copyFeedURL");
620 break;
621 case 1:
622 if (selectedRows[0] === 0) {
623 // menu when "unread" feed selected
624 this.showItem("update");
625 this.showItem("markRead");
626 this.hideItem("rename");
627 this.hideItem("delete");
628 this.showItem("newSubscription");
629 this.hideItem("newFolder");
630 this.hideItem("updateAll");
631 this.hideItem("copyFeedURL");
633 else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === "") {
634 // menu when single folder selected
635 this.showItem("update");
636 this.showItem("markRead");
637 this.showItem("rename");
638 this.showItem("delete");
639 this.showItem("newSubscription");
640 this.showItem("newFolder");
641 this.hideItem("updateAll");
642 this.hideItem("copyFeedURL");
644 else {
645 // menu when single feed selected
646 this.showItem("update");
647 this.showItem("markRead");
648 this.showItem("rename");
649 this.showItem("delete");
650 this.showItem("newSubscription");
651 this.hideItem("newFolder");
652 this.hideItem("updateAll");
653 this.showItem("copyFeedURL");
655 break;
656 default:
657 // menu when multiple items selected
658 this.showItem("update");
659 this.showItem("markRead");
660 this.hideItem("rename");
661 this.showItem("delete");
662 this.hideItem("newSubscription");
663 this.hideItem("newFolder");
664 this.hideItem("updateAll");
665 this.showItem("copyFeedURL");
666 break;
671 const RssArticleContextMenu = new Class({
672 Extends: ContextMenu
675 const RssDownloaderRuleContextMenu = new Class({
676 Extends: ContextMenu,
677 adjustMenuPosition: function(e) {
678 this.updateMenuItems();
680 // draw the menu off-screen to know the menu dimensions
681 this.menu.style.left = "-999em";
682 this.menu.style.top = "-999em";
683 // position the menu
684 let xPosMenu = e.pageX + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
685 let yPosMenu = e.pageY + this.options.offsets.y - $("rssdownloaderpage").offsetTop;
686 if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
687 xPosMenu -= this.menu.offsetWidth;
688 if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
689 yPosMenu = document.documentElement.clientHeight - this.menu.offsetHeight;
690 xPosMenu = Math.max(xPosMenu, 0);
691 yPosMenu = Math.max(yPosMenu, 0);
693 this.menu.style.left = `${xPosMenu}px`;
694 this.menu.style.top = `${yPosMenu}px`;
695 this.menu.style.position = "absolute";
696 this.menu.style.zIndex = "2000";
698 updateMenuItems: function() {
699 const selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
700 this.showItem("addRule");
701 switch (selectedRows.length) {
702 case 0:
703 // menu when nothing selected
704 this.hideItem("deleteRule");
705 this.hideItem("renameRule");
706 this.hideItem("clearDownloadedEpisodes");
707 break;
708 case 1:
709 // menu when single item selected
710 this.showItem("deleteRule");
711 this.showItem("renameRule");
712 this.showItem("clearDownloadedEpisodes");
713 break;
714 default:
715 // menu when multiple items selected
716 this.showItem("deleteRule");
717 this.hideItem("renameRule");
718 this.showItem("clearDownloadedEpisodes");
719 break;
724 return exports();
725 })();
726 Object.freeze(window.qBittorrent.ContextMenu);