Scroll projector to a given line
This commit is contained in:
parent
011ec56b88
commit
252ba02e86
@ -7,6 +7,9 @@ https://openslides.org/
|
|||||||
Version 2.3 (unreleased)
|
Version 2.3 (unreleased)
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
Motions:
|
||||||
|
- New feature to schroll the projector to a specific line [#3748].
|
||||||
|
|
||||||
|
|
||||||
Version 2.2 (2018-06-06)
|
Version 2.2 (2018-06-06)
|
||||||
========================
|
========================
|
||||||
|
@ -485,7 +485,7 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
deregisterCallbacks: function () {
|
deregisterCallbacks: function () {
|
||||||
_.forEach(arguments, this.deregisterCallback);
|
_.forEach(arguments, this.deregisterCallback);
|
||||||
},
|
},
|
||||||
notify: function(eventName, params, users, channels) {
|
notify: function(eventName, params, users, channels, projectors) {
|
||||||
if (eventNameRegex.test(eventName)) {
|
if (eventNameRegex.test(eventName)) {
|
||||||
if (!params || typeof params !== 'object') {
|
if (!params || typeof params !== 'object') {
|
||||||
params = {};
|
params = {};
|
||||||
@ -497,6 +497,7 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
params: params,
|
params: params,
|
||||||
users: users,
|
users: users,
|
||||||
replyChannels: channels,
|
replyChannels: channels,
|
||||||
|
projectors: projectors,
|
||||||
};
|
};
|
||||||
if (!operator.user) {
|
if (!operator.user) {
|
||||||
if (!anonymousTrackId) {
|
if (!anonymousTrackId) {
|
||||||
|
@ -21,13 +21,40 @@ angular.module('OpenSlidesApp.motions.projector', [
|
|||||||
'Motion',
|
'Motion',
|
||||||
'MotionChangeRecommendation',
|
'MotionChangeRecommendation',
|
||||||
'User',
|
'User',
|
||||||
function($scope, Motion, MotionChangeRecommendation, User) {
|
'Notify',
|
||||||
|
'ProjectorID',
|
||||||
|
function($scope, Motion, MotionChangeRecommendation, User, Notify, ProjectorID) {
|
||||||
// Attention! Each object that is used here has to be dealt on server side.
|
// Attention! Each object that is used here has to be dealt on server side.
|
||||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||||
// class.
|
// class.
|
||||||
var id = $scope.element.id;
|
var id = $scope.element.id;
|
||||||
$scope.mode = $scope.element.mode || 'original';
|
$scope.mode = $scope.element.mode || 'original';
|
||||||
|
|
||||||
|
var notifyNamePrefix = 'projector_' + ProjectorID() + '_motion_line_';
|
||||||
|
var callbackId = Notify.registerCallback(notifyNamePrefix + 'request', function (params) {
|
||||||
|
var line = params.params.line;
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollTop = null;
|
||||||
|
$('.line-number-' + line).each(function() {
|
||||||
|
var top = $(this).offset().top;
|
||||||
|
if (scrollTop === null || top < scrollTop) {
|
||||||
|
scrollTop = top;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (scrollTop) {
|
||||||
|
scrollTop += (-$scope.scroll); // Add the (reversed) scrolling ontop
|
||||||
|
var scroll = Math.floor((scrollTop/250) - 0.2);
|
||||||
|
var channel = params.senderReplyChannelName;
|
||||||
|
Notify.notify(notifyNamePrefix + 'answer', {scroll: scroll}, null, [channel], null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.$on('$destroy', function () {
|
||||||
|
Notify.deregisterCallback(callbackId);
|
||||||
|
});
|
||||||
|
|
||||||
Motion.bindOne(id, $scope, 'motion');
|
Motion.bindOne(id, $scope, 'motion');
|
||||||
User.bindAll({}, $scope, 'users');
|
User.bindAll({}, $scope, 'users');
|
||||||
|
|
||||||
|
@ -1415,6 +1415,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'MotionBlock',
|
'MotionBlock',
|
||||||
'MotionPdfExport',
|
'MotionPdfExport',
|
||||||
'PersonalNoteManager',
|
'PersonalNoteManager',
|
||||||
|
'Notify',
|
||||||
'WebpageTitle',
|
'WebpageTitle',
|
||||||
'EditingWarning',
|
'EditingWarning',
|
||||||
function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
|
function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
|
||||||
@ -1422,7 +1423,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment,
|
MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment,
|
||||||
Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing,
|
Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing,
|
||||||
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock,
|
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock,
|
||||||
MotionPdfExport, PersonalNoteManager, WebpageTitle, EditingWarning) {
|
MotionPdfExport, PersonalNoteManager, Notify, WebpageTitle, EditingWarning) {
|
||||||
var motion = Motion.get(motionId);
|
var motion = Motion.get(motionId);
|
||||||
Category.bindAll({}, $scope, 'categories');
|
Category.bindAll({}, $scope, 'categories');
|
||||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||||
@ -1538,13 +1539,16 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.scrollToLine = 0;
|
||||||
$scope.highlight = 0;
|
$scope.highlight = 0;
|
||||||
|
$scope.linesForProjector = false;
|
||||||
$scope.scrollToAndHighlight = function (line) {
|
$scope.scrollToAndHighlight = function (line) {
|
||||||
|
$scope.scrollToLine = line;
|
||||||
$scope.highlight = line;
|
$scope.highlight = line;
|
||||||
|
|
||||||
// The same line number can occur twice in diff view; we scroll to the first one in this case
|
// The same line number can occur twice in diff view; we scroll to the first one in this case
|
||||||
var scrollTop = null;
|
var scrollTop = null;
|
||||||
$(".line-number-" + line).each(function() {
|
$('.line-number-' + line).each(function() {
|
||||||
var top = $(this).offset().top;
|
var top = $(this).offset().top;
|
||||||
if (top > 0 && (scrollTop === null || top < scrollTop)) {
|
if (top > 0 && (scrollTop === null || top < scrollTop)) {
|
||||||
scrollTop = top;
|
scrollTop = top;
|
||||||
@ -1561,6 +1565,29 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.highlight = 0;
|
$scope.highlight = 0;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.scrollProjectorToLine(line);
|
||||||
|
};
|
||||||
|
$scope.scrollProjectorToLine = function (line) {
|
||||||
|
var projectorIds = $scope.motion.isProjected();
|
||||||
|
if (!$scope.linesForProjector || !line || !projectorIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var projectorId = projectorIds[0];
|
||||||
|
var notifyNamePrefix = 'projector_' + projectorId + '_motion_line_';
|
||||||
|
|
||||||
|
// register callback
|
||||||
|
var callbackId = Notify.registerCallback(notifyNamePrefix + 'answer', function (params) {
|
||||||
|
Notify.deregisterCallback(callbackId);
|
||||||
|
$http.post('/rest/core/projector/' + projectorId + '/set_scroll/', params.params.scroll);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query all projectors
|
||||||
|
Notify.notify(notifyNamePrefix + 'request', {line: line}, null, null, [projectorId]);
|
||||||
|
};
|
||||||
|
$scope.toggleLinesForProjector = function () {
|
||||||
|
$scope.linesForProjector = !$scope.linesForProjector;
|
||||||
|
$scope.scrollProjectorToLine($scope.scrollToLine);
|
||||||
};
|
};
|
||||||
|
|
||||||
// open edit dialog
|
// open edit dialog
|
||||||
|
@ -79,14 +79,20 @@
|
|||||||
e-formclass="small-form"
|
e-formclass="small-form"
|
||||||
onaftersave="scrollToAndHighlight(gotoLinenumber)">
|
onaftersave="scrollToAndHighlight(gotoLinenumber)">
|
||||||
</span>
|
</span>
|
||||||
<button type="button" class="btn btn-sm btn-default"
|
<div class="btn-group" ng-if="lineNumberMode != 'none'">
|
||||||
ng-click="lineNumberForm.$show()"
|
<button type="button" class="btn btn-sm btn-default" ng-click="lineNumberForm.$show()">
|
||||||
ng-if="lineNumberMode != 'none'"
|
<i class="fa fa-share"></i>
|
||||||
uib-tooltip="{{ 'Jump to a given line number' | translate }}"
|
<translate>go</translate>
|
||||||
tooltip-placement="bottom">
|
</button>
|
||||||
<i class="fa fa-share"></i>
|
<button type="button" class="btn btn-sm btn-default"
|
||||||
<translate>go</translate>
|
ng-if="lineNumberMode != 'none' && motion.isProjected().length &&
|
||||||
</button>
|
operator.hasPerms('core.can_manage_projector')"
|
||||||
|
ng-click="toggleLinesForProjector()"
|
||||||
|
uib-tooltip="{{ 'Show highlighted line also on projector.' | translate }}">
|
||||||
|
<i class="fa" ng-class="linesForProjector ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||||
|
<i class="fa fa-video-camera"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,12 +7,14 @@ from openslides.utils.autoupdate import (
|
|||||||
ws_add_site,
|
ws_add_site,
|
||||||
ws_disconnect_projector,
|
ws_disconnect_projector,
|
||||||
ws_disconnect_site,
|
ws_disconnect_site,
|
||||||
|
ws_receive_projector,
|
||||||
ws_receive_site,
|
ws_receive_site,
|
||||||
)
|
)
|
||||||
|
|
||||||
projector_routing = [
|
projector_routing = [
|
||||||
route("websocket.connect", ws_add_projector),
|
route("websocket.connect", ws_add_projector),
|
||||||
route("websocket.disconnect", ws_disconnect_projector),
|
route("websocket.disconnect", ws_disconnect_projector),
|
||||||
|
route("websocket.receive", ws_receive_projector),
|
||||||
]
|
]
|
||||||
|
|
||||||
site_routing = [
|
site_routing = [
|
||||||
|
@ -123,9 +123,8 @@ def ws_disconnect_site(message: Any) -> None:
|
|||||||
@channel_session_user
|
@channel_session_user
|
||||||
def ws_receive_site(message: Any) -> None:
|
def ws_receive_site(message: Any) -> None:
|
||||||
"""
|
"""
|
||||||
This function is called if a message from a client comes in. The message
|
If we recieve something from the client we currently just interpret this
|
||||||
should be a list. Every item is broadcasted to the given users (or all
|
as a notify message.
|
||||||
users if no user list is given) if it is a notify element.
|
|
||||||
|
|
||||||
The server adds the sender's user id (0 for anonymous) and reply
|
The server adds the sender's user id (0 for anonymous) and reply
|
||||||
channel name so that a receiver client may reply to the sender or to all
|
channel name so that a receiver client may reply to the sender or to all
|
||||||
@ -138,51 +137,76 @@ def ws_receive_site(message: Any) -> None:
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if isinstance(incomming, list):
|
if isinstance(incomming, list):
|
||||||
# Parse all items
|
notify(
|
||||||
receivers_users = defaultdict(list) # type: Dict[int, List[Any]]
|
incomming,
|
||||||
receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]]
|
senderReplyChannelName=message.reply_channel.name,
|
||||||
items_for_all = []
|
senderUserId=message.user.id or 0)
|
||||||
for item in incomming:
|
|
||||||
if item.get('collection') == 'notify':
|
|
||||||
use_receivers_dict = False
|
|
||||||
item['senderReplyChannelName'] = message.reply_channel.name
|
|
||||||
item['senderUserId'] = message.user.id or 0
|
|
||||||
|
|
||||||
# Force the params to be a dict
|
|
||||||
if not isinstance(item.get('params'), dict):
|
|
||||||
item['params'] = {}
|
|
||||||
|
|
||||||
users = item.get('users')
|
def notify(incomming: List[Dict[str, Any]], **attributes: Any) -> None:
|
||||||
if isinstance(users, list):
|
"""
|
||||||
# Send this item only to all reply channels of some site users.
|
The incomming should be a list of notify elements. Every item is broadcasted
|
||||||
for user_id in users:
|
to the given users, channels or projectors. If none is given, the message is
|
||||||
receivers_users[user_id].append(item)
|
send to each site client.
|
||||||
use_receivers_dict = True
|
"""
|
||||||
|
# Parse all items
|
||||||
|
receivers_users = defaultdict(list) # type: Dict[int, List[Any]]
|
||||||
|
receivers_projectors = defaultdict(list) # type: Dict[int, List[Any]]
|
||||||
|
receivers_reply_channels = defaultdict(list) # type: Dict[str, List[Any]]
|
||||||
|
items_for_all = []
|
||||||
|
for item in incomming:
|
||||||
|
if item.get('collection') == 'notify':
|
||||||
|
use_receivers_dict = False
|
||||||
|
|
||||||
reply_channels = item.get('replyChannels')
|
for key, value in attributes.items():
|
||||||
if isinstance(reply_channels, list):
|
item[key] = value
|
||||||
# Send this item only to some reply channels.
|
|
||||||
for reply_channel_name in reply_channels:
|
|
||||||
receivers_reply_channels[reply_channel_name].append(item)
|
|
||||||
use_receivers_dict = True
|
|
||||||
|
|
||||||
if not use_receivers_dict:
|
# Force the params to be a dict
|
||||||
# Send this item to all reply channels.
|
if not isinstance(item.get('params'), dict):
|
||||||
items_for_all.append(item)
|
item['params'] = {}
|
||||||
|
|
||||||
# Send all items
|
users = item.get('users')
|
||||||
for user_id, channel_names in websocket_user_cache.get_all().items():
|
if isinstance(users, list):
|
||||||
output = receivers_users[user_id]
|
# Send this item only to all reply channels of some site users.
|
||||||
if len(output) > 0:
|
for user_id in users:
|
||||||
for channel_name in channel_names:
|
receivers_users[user_id].append(item)
|
||||||
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
|
use_receivers_dict = True
|
||||||
|
|
||||||
for channel_name, output in receivers_reply_channels.items():
|
projectors = item.get('projectors')
|
||||||
if len(output) > 0:
|
if isinstance(projectors, list):
|
||||||
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
|
# Send this item only to all reply channels of some site users.
|
||||||
|
for projector_id in projectors:
|
||||||
|
receivers_projectors[projector_id].append(item)
|
||||||
|
use_receivers_dict = True
|
||||||
|
|
||||||
if len(items_for_all) > 0:
|
reply_channels = item.get('replyChannels')
|
||||||
send_or_wait(Group('site').send, {'text': json.dumps(items_for_all)})
|
if isinstance(reply_channels, list):
|
||||||
|
# Send this item only to some reply channels.
|
||||||
|
for reply_channel_name in reply_channels:
|
||||||
|
receivers_reply_channels[reply_channel_name].append(item)
|
||||||
|
use_receivers_dict = True
|
||||||
|
|
||||||
|
if not use_receivers_dict:
|
||||||
|
# Send this item to all reply channels.
|
||||||
|
items_for_all.append(item)
|
||||||
|
|
||||||
|
# Send all items
|
||||||
|
for user_id, channel_names in websocket_user_cache.get_all().items():
|
||||||
|
output = receivers_users[user_id]
|
||||||
|
if len(output) > 0:
|
||||||
|
for channel_name in channel_names:
|
||||||
|
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
|
||||||
|
|
||||||
|
for channel_name, output in receivers_reply_channels.items():
|
||||||
|
if len(output) > 0:
|
||||||
|
send_or_wait(Channel(channel_name).send, {'text': json.dumps(output)})
|
||||||
|
|
||||||
|
for projector_id, output in receivers_projectors.items():
|
||||||
|
if len(output) > 0:
|
||||||
|
send_or_wait(Group('projector-{}'.format(projector_id)).send, {'text': json.dumps(output)})
|
||||||
|
|
||||||
|
if len(items_for_all) > 0:
|
||||||
|
send_or_wait(Group('site').send, {'text': json.dumps(items_for_all)})
|
||||||
|
|
||||||
|
|
||||||
@channel_session_user_from_http
|
@channel_session_user_from_http
|
||||||
@ -247,6 +271,27 @@ def ws_disconnect_projector(message: Any, projector_id: int) -> None:
|
|||||||
Group('projector-all').discard(message.reply_channel)
|
Group('projector-all').discard(message.reply_channel)
|
||||||
|
|
||||||
|
|
||||||
|
def ws_receive_projector(message: Any, projector_id: int) -> None:
|
||||||
|
"""
|
||||||
|
If we recieve something from the client we currently just interpret this
|
||||||
|
as a notify message.
|
||||||
|
|
||||||
|
The server adds the sender's projector id and reply channel name so that
|
||||||
|
a receiver client may reply to the sender or to all sender's instances.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
incomming = json.loads(message.content['text'])
|
||||||
|
except ValueError:
|
||||||
|
# Message content is invalid. Just do nothing.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(incomming, list):
|
||||||
|
notify(
|
||||||
|
incomming,
|
||||||
|
senderReplyChannelName=message.reply_channel.name,
|
||||||
|
senderProjectorId=projector_id)
|
||||||
|
|
||||||
|
|
||||||
def send_data_projector(message: ChannelMessageFormat) -> None:
|
def send_data_projector(message: ChannelMessageFormat) -> None:
|
||||||
"""
|
"""
|
||||||
Informs all projector clients about changed data.
|
Informs all projector clients about changed data.
|
||||||
|
Loading…
Reference in New Issue
Block a user