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.
31 let lastShownContextMenu
= null;
32 const ContextMenu
= new Class({
34 Implements
: [Options
, Events
],
54 initialize: function(options
) {
56 this.setOptions(options
);
59 this.menu
= $(this.options
.menu
);
60 this.targets
= $$(this.options
.targets
);
63 this.fx
= new Fx
.Tween(this.menu
, {
65 duration
: this.options
.fadeSpeed
,
66 onComplete: function() {
67 if (this.getStyle('opacity')) {
68 this.setStyle('visibility', 'visible');
71 this.setStyle('visibility', 'hidden');
76 //hide and begin the listener
77 this.hide().startListener();
81 'position': 'absolute',
87 adjustMenuPosition: function(e
) {
88 this.updateMenuItems();
90 const scrollableMenuMaxHeight
= document
.documentElement
.clientHeight
* 0.75;
92 if (this.menu
.hasClass('scrollableMenu'))
93 this.menu
.setStyle('max-height', scrollableMenuMaxHeight
);
95 // draw the menu off-screen to know the menu dimensions
102 let xPosMenu
= e
.page
.x
+ this.options
.offsets
.x
;
103 let yPosMenu
= e
.page
.y
+ this.options
.offsets
.y
;
104 if (xPosMenu
+ this.menu
.offsetWidth
> document
.documentElement
.clientWidth
)
105 xPosMenu
-= this.menu
.offsetWidth
;
106 if (yPosMenu
+ this.menu
.offsetHeight
> document
.documentElement
.clientHeight
)
107 yPosMenu
= document
.documentElement
.clientHeight
- this.menu
.offsetHeight
;
112 this.menu
.setStyles({
115 position
: 'absolute',
119 // position the sub-menu
120 const uls
= this.menu
.getElementsByTagName('ul');
121 for (let i
= 0; i
< uls
.length
; ++i
) {
123 if (ul
.hasClass('scrollableMenu'))
124 ul
.setStyle('max-height', scrollableMenuMaxHeight
);
125 const rectParent
= ul
.parentNode
.getBoundingClientRect();
126 const xPosOrigin
= rectParent
.left
;
127 const yPosOrigin
= rectParent
.bottom
;
128 let xPos
= xPosOrigin
+ rectParent
.width
- 1;
129 let yPos
= yPosOrigin
- rectParent
.height
- 1;
130 if (xPos
+ ul
.offsetWidth
> document
.documentElement
.clientWidth
)
131 xPos
-= (ul
.offsetWidth
+ rectParent
.width
- 2);
132 if (yPos
+ ul
.offsetHeight
> document
.documentElement
.clientHeight
)
133 yPos
= document
.documentElement
.clientHeight
- ul
.offsetHeight
;
139 'margin-left': xPos
- xPosOrigin
,
140 'margin-top': yPos
- yPosOrigin
145 setupEventListeners: function(elem
) {
146 elem
.addEvent('contextmenu', function(e
) {
147 this.triggerMenu(e
, elem
);
149 elem
.addEvent('click', function(e
) {
153 elem
.addEvent('touchstart', function(e
) {
155 clearTimeout(this.touchstartTimer
);
158 const touchstartEvent
= e
;
159 this.touchstartTimer
= setTimeout(function() {
160 this.triggerMenu(touchstartEvent
, elem
);
161 }.bind(this), this.options
.touchTimer
);
163 elem
.addEvent('touchend', function(e
) {
165 clearTimeout(this.touchstartTimer
);
169 addTarget: function(t
) {
170 this.targets
[this.targets
.length
] = t
;
171 this.setupEventListeners(t
);
174 triggerMenu: function(e
, el
) {
175 if (this.options
.disabled
)
178 //prevent default, if told to
179 if (this.options
.stopEvent
) {
182 //record this as the trigger
183 this.options
.element
= $(el
);
184 this.adjustMenuPosition(e
);
190 startListener: function() {
192 this.targets
.each(function(el
) {
193 this.setupEventListeners(el
);
197 this.menu
.getElements('a').each(function(item
) {
198 item
.addEvent('click', function(e
) {
200 if (!item
.hasClass('disabled')) {
201 this.execute(item
.get('href').split('#')[1], $(this.options
.element
));
202 this.fireEvent('click', [item
, e
]);
208 $(document
.body
).addEvent('click', function() {
213 updateMenuItems: function() {},
216 show: function(trigger
) {
217 if (lastShownContextMenu
&& lastShownContextMenu
!= this)
218 lastShownContextMenu
.hide();
220 this.fireEvent('show');
222 lastShownContextMenu
= this;
227 hide: function(trigger
) {
230 //this.menu.fade('out');
231 this.fireEvent('hide');
237 setItemChecked: function(item
, checked
) {
238 this.menu
.getElement('a[href$=' + item
+ ']').firstChild
.style
.opacity
=
243 getItemChecked: function(item
) {
244 return '0' != this.menu
.getElement('a[href$=' + item
+ ']').firstChild
.style
.opacity
;
248 hideItem: function(item
) {
249 this.menu
.getElement('a[href$=' + item
+ ']').parentNode
.addClass('invisible');
254 showItem: function(item
) {
255 this.menu
.getElement('a[href$=' + item
+ ']').parentNode
.removeClass('invisible');
259 //disable the entire menu
260 disable: function() {
261 this.options
.disabled
= true;
265 //enable the entire menu
267 this.options
.disabled
= false;
272 execute: function(action
, element
) {
273 if (this.options
.actions
[action
]) {
274 this.options
.actions
[action
](element
, this, action
);
280 const TorrentsTableContextMenu
= new Class({
281 Extends
: ContextMenu
,
283 updateMenuItems: function() {
284 let all_are_seq_dl
= true;
285 let there_are_seq_dl
= false;
286 let all_are_f_l_piece_prio
= true;
287 let there_are_f_l_piece_prio
= false;
288 let all_are_downloaded
= true;
289 let all_are_paused
= true;
290 let there_are_paused
= false;
291 let all_are_force_start
= true;
292 let there_are_force_start
= false;
293 let all_are_super_seeding
= true;
294 let all_are_auto_tmm
= true;
295 let there_are_auto_tmm
= false;
296 const tagsSelectionState
= Object
.clone(tagList
);
298 const h
= torrentsTable
.selectedRowsIds();
299 h
.each(function(item
, index
) {
300 const data
= torrentsTable
.rows
.get(item
).full_data
;
302 if (data
['seq_dl'] !== true)
303 all_are_seq_dl
= false;
305 there_are_seq_dl
= true;
307 if (data
['f_l_piece_prio'] !== true)
308 all_are_f_l_piece_prio
= false;
310 there_are_f_l_piece_prio
= true;
312 if (data
['progress'] != 1.0) // not downloaded
313 all_are_downloaded
= false;
314 else if (data
['super_seeding'] !== true)
315 all_are_super_seeding
= false;
317 if (data
['state'] != 'pausedUP' && data
['state'] != 'pausedDL')
318 all_are_paused
= false;
320 there_are_paused
= true;
322 if (data
['force_start'] !== true)
323 all_are_force_start
= false;
325 there_are_force_start
= true;
327 if (data
['auto_tmm'] === true)
328 there_are_auto_tmm
= true;
330 all_are_auto_tmm
= false;
332 const torrentTags
= data
['tags'].split(', ');
333 for (const key
in tagsSelectionState
) {
334 const tag
= tagsSelectionState
[key
];
335 const tagExists
= torrentTags
.contains(tag
.name
);
336 if ((tag
.checked
!== undefined) && (tag
.checked
!= tagExists
))
337 tag
.indeterminate
= true;
338 if (tag
.checked
=== undefined)
339 tag
.checked
= tagExists
;
341 tag
.checked
= tag
.checked
&& tagExists
;
345 let show_seq_dl
= true;
347 if (!all_are_seq_dl
&& there_are_seq_dl
)
350 let show_f_l_piece_prio
= true;
352 if (!all_are_f_l_piece_prio
&& there_are_f_l_piece_prio
)
353 show_f_l_piece_prio
= false;
355 if (all_are_downloaded
) {
356 this.hideItem('downloadLimit');
357 this.menu
.getElement('a[href$=uploadLimit]').parentNode
.addClass('separator');
358 this.hideItem('sequentialDownload');
359 this.hideItem('firstLastPiecePrio');
360 this.showItem('superSeeding');
361 this.setItemChecked('superSeeding', all_are_super_seeding
);
364 if (!show_seq_dl
&& show_f_l_piece_prio
)
365 this.menu
.getElement('a[href$=firstLastPiecePrio]').parentNode
.addClass('separator');
367 this.menu
.getElement('a[href$=firstLastPiecePrio]').parentNode
.removeClass('separator');
370 this.showItem('sequentialDownload');
372 this.hideItem('sequentialDownload');
374 if (show_f_l_piece_prio
)
375 this.showItem('firstLastPiecePrio');
377 this.hideItem('firstLastPiecePrio');
379 this.setItemChecked('sequentialDownload', all_are_seq_dl
);
380 this.setItemChecked('firstLastPiecePrio', all_are_f_l_piece_prio
);
382 this.showItem('downloadLimit');
383 this.menu
.getElement('a[href$=uploadLimit]').parentNode
.removeClass('separator');
384 this.hideItem('superSeeding');
387 this.showItem('start');
388 this.showItem('pause');
389 this.showItem('forceStart');
391 this.hideItem('pause');
392 else if (all_are_force_start
)
393 this.hideItem('forceStart');
394 else if (!there_are_paused
&& !there_are_force_start
)
395 this.hideItem('start');
397 if (!all_are_auto_tmm
&& there_are_auto_tmm
) {
398 this.hideItem('autoTorrentManagement');
401 this.showItem('autoTorrentManagement');
402 this.setItemChecked('autoTorrentManagement', all_are_auto_tmm
);
405 const contextTagList
= $('contextTagList');
406 for (const tagHash
in tagList
) {
407 const checkbox
= contextTagList
.getElement('a[href=#Tag/' + tagHash
+ '] input[type=checkbox]');
408 const checkboxState
= tagsSelectionState
[tagHash
];
409 checkbox
.indeterminate
= checkboxState
.indeterminate
;
410 checkbox
.checked
= checkboxState
.checked
;
414 updateCategoriesSubMenu: function(category_list
) {
415 const categoryList
= $('contextCategoryList');
416 categoryList
.empty();
417 categoryList
.appendChild(new Element('li', {
418 html
: '<a href="javascript:torrentNewCategoryFN();"><img src="images/qbt-theme/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>'
420 categoryList
.appendChild(new Element('li', {
421 html
: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/qbt-theme/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>'
424 const sortedCategories
= [];
425 Object
.each(category_list
, function(category
) {
426 sortedCategories
.push(category
.name
);
428 sortedCategories
.sort();
431 Object
.each(sortedCategories
, function(categoryName
) {
432 const categoryHash
= genHash(categoryName
);
433 const el
= new Element('li', {
434 html
: '<a href="javascript:torrentSetCategoryFN(\'' + categoryHash
+ '\');"><img src="images/qbt-theme/inode-directory.svg"/> ' + escapeHtml(categoryName
) + '</a>'
437 el
.addClass('separator');
440 categoryList
.appendChild(el
);
444 updateTagsSubMenu: function(tagList
) {
445 const contextTagList
= $('contextTagList');
446 while (contextTagList
.firstChild
!== null)
447 contextTagList
.removeChild(contextTagList
.firstChild
);
449 contextTagList
.appendChild(new Element('li', {
450 html
: '<a href="javascript:torrentAddTagsFN();">'
451 + '<img src="images/qbt-theme/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>'
452 + ' QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]'
455 contextTagList
.appendChild(new Element('li', {
456 html
: '<a href="javascript:torrentRemoveAllTagsFN();">'
457 + '<img src="images/qbt-theme/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>'
458 + ' QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]'
462 const sortedTags
= [];
463 for (const key
in tagList
)
464 sortedTags
.push(tagList
[key
].name
);
467 for (let i
= 0; i
< sortedTags
.length
; ++i
) {
468 const tagName
= sortedTags
[i
];
469 const tagHash
= genHash(tagName
);
470 const el
= new Element('li', {
471 html
: '<a href="#Tag/' + tagHash
+ '" onclick="event.preventDefault(); torrentSetTagsFN(\'' + tagHash
+ '\', !event.currentTarget.getElement(\'input[type=checkbox]\').checked);">'
472 + '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + escapeHtml(tagName
)
476 el
.addClass('separator');
477 contextTagList
.appendChild(el
);
482 const CategoriesFilterContextMenu
= new Class({
483 Extends
: ContextMenu
,
484 updateMenuItems: function() {
485 const id
= this.options
.element
.id
;
486 if ((id
!= CATEGORIES_ALL
) && (id
!= CATEGORIES_UNCATEGORIZED
)) {
487 this.showItem('editCategory');
488 this.showItem('deleteCategory');
491 this.hideItem('editCategory');
492 this.hideItem('deleteCategory');
497 const TagsFilterContextMenu
= new Class({
498 Extends
: ContextMenu
,
499 updateMenuItems: function() {
500 const id
= this.options
.element
.id
;
501 if ((id
!== TAGS_ALL
.toString()) && (id
!== TAGS_UNTAGGED
.toString()))
502 this.showItem('deleteTag');
504 this.hideItem('deleteTag');
508 const SearchPluginsTableContextMenu
= new Class({
509 Extends
: ContextMenu
,
511 updateMenuItems: function() {
512 const enabledColumnIndex = function(text
) {
513 const columns
= $("searchPluginsTableFixedHeaderRow").getChildren("th");
514 for (let i
= 0; i
< columns
.length
; ++i
)
515 if (columns
[i
].get("html") === "Enabled")
519 this.showItem('Enabled');
520 this.setItemChecked('Enabled', this.options
.element
.getChildren("td")[enabledColumnIndex()].get("html") === "Yes");
522 this.showItem('Uninstall');