def on_activate(self, mi): obj = self.model().object_data[mi.row()] can_mcr = has_right("mcr", self.id_channel) if obj.object_type == "item": if obj.id: if obj["item_role"] == "placeholder": self.on_edit_item() elif self.parent().mcr and self.parent().mcr.isVisible( ) and can_mcr: response = api.playout( timeout=1, action="cue", id_channel=self.id_channel, id_item=obj.id, ) if not response: logging.error(response.message) self.clearSelection() # Event edit elif obj.object_type == "event" and ( has_right("scheduler_view", self.id_channel) or has_right("scheduler_edit", self.id_channel)): self.on_edit_event() self.clearSelection()
def on_delete_event(self): if not self.calendar.selected_event: return cursor_event = self.calendar.selected_event if not has_right("scheduler_edit", self.id_channel): logging.error( "You are not allowed to modify schedule of this channel.") return ret = QMessageBox.question( self, "Delete event", f"Do you really want to delete {cursor_event}?" "\nThis operation cannot be undone.", QMessageBox.Yes | QMessageBox.No, ) if ret == QMessageBox.Yes: QApplication.processEvents() self.calendar.setCursor(Qt.WaitCursor) response = api.schedule( id_channel=self.id_channel, start_time=self.calendar.week_start_time, end_time=self.calendar.week_end_time, delete=[cursor_event.id], ) self.calendar.setCursor(Qt.ArrowCursor) if response: logging.info(f"{cursor_event} deleted") self.calendar.set_data(response.data) else: logging.error(response.message) self.calendar.load()
def on_channel_changed(self): if hasattr(self, "id_channel"): can_mcr = has_right("mcr", self.id_channel) self.btn_take.setEnabled(can_mcr) self.btn_freeze.setEnabled(can_mcr) self.btn_retake.setEnabled(can_mcr) self.btn_abort.setEnabled(can_mcr) self.btn_loop.setEnabled(can_mcr) self.btn_cue_backward.setEnabled(can_mcr) self.btn_cue_forward.setEnabled(can_mcr) if hasattr(self, "plugins"): self.plugins.load() self.pos = 0 self.dur = 0 self.current = "(loading)" self.cued = "(loading)" self.request_time = 0 self.paused = False self.cueing = False self.local_request_time = time.time() self.updating = False self.request_display_resize = False self.first_update = True self.fps = 25.0 self.parent().setWindowTitle("On air ctrl") self.progress_bar.setValue(0) self.progress_bar.setMaximum(0) self.display_current.set_text( f"<font color='yellow'>{self.current}</font>") self.display_cued.set_text(f"<font color='yellow'>{self.cued}</font>")
def dragTargetChanged(self, evt): if not has_right("scheduler_edit", self.calendar.id_channel): return if type(evt) == SchedulerDayWidget: self.drag_outside = False else: self.drag_outside = True self.calendar.drag_source.update()
def on_channel_changed(self): self.load(do_update_header=True) self.plugins.load() if self.mcr: self.mcr.on_channel_changed() can_rundown_edit = has_right("rundown_edit", self.id_channel) self.main_window.action_rundown_edit.setEnabled(can_rundown_edit) self.toggle_rundown_edit(can_rundown_edit and self.edit_wanted)
def focus(self, asset, silent=False, force=False): if not isinstance(asset, Asset): return logging.debug(f"[DETAIL] Focusing {asset}") if self._is_loading: self._load_queue = [asset] return else: self._load_queue = False self._is_loading = True if not silent: self.check_changed() # # Show data # self.folder_select.setEnabled(True) self.asset = Asset(meta=asset.meta) # asset deep copy self.parent().setWindowTitle(f"Detail of {self.asset}") self.detail_tabs.load(self.asset, force=force) self.folder_select.set_value(self.asset["id_folder"]) self.duration.fps = self.asset.fps self.duration.set_value(self.asset.duration) self.duration.show() if (self.asset["status"] == ObjectStatus.OFFLINE) or (not self.asset.id): self.duration.setEnabled(True) else: self.duration.setEnabled(False) enabled = (not asset.id) or has_right("asset_edit", self.asset["id_folder"]) self.folder_select.setEnabled(enabled) self.action_approve.setEnabled(enabled) self.action_qc_reset.setEnabled(enabled) self.action_reject.setEnabled(enabled) self.action_apply.setEnabled(enabled) self.action_revert.setEnabled(enabled) self.set_title("DETAIL : " + self.asset.__repr__()) self._is_loading = False if self._load_queue: self.focus(self._load_queue)
def dragMoveEvent(self, evt): if not has_right("scheduler_edit", self.calendar.id_channel): return self.dragging = True self.calendar.focus_data = [] self.mx = evt.pos().x() self.my = evt.pos().y() cursor_time = (self.my / self.min_size * 60) + self.start_time if self.round_ts(cursor_time) != self.round_ts(self.cursor_time): self.cursor_time = cursor_time self.update() # disallow droping event over another event if type(self.calendar.dragging) == Event: if self.round_ts(self.cursor_time - self.calendar.drag_offset) in [ event["start"] for event in self.calendar.events ]: evt.ignore() return evt.accept()
def load(self): if not has_right("mcr", self.id_channel): return logging.debug("[PLUGINS] Loading playout plugins") for idx in reversed(range(self.count())): widget = self.widget(idx) self.removeTab(idx) widget.deleteLater() response = api.playout(action="plugin_list", id_channel=self.id_channel) if not response: logging.error( f"[PLUGINS] Unable to load playout plugins:\n{response.message}" ) return for plugin in response.data or []: self.plugins.append(PlayoutPlugin(self, plugin)) self.addTab(self.plugins[-1], plugin.get("title", "unknown"))
def __init__(self, parent): super(RundownModule, self).__init__(parent) self.start_time = 0 self.current_item = False self.cued_item = False self.last_search = "" self.first_load = True self.edit_wanted = self.app_state.get("edit_enabled", True) self.edit_enabled = False self.toolbar = rundown_toolbar(self) layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(2) layout.addWidget(self.toolbar, 0) self.view = RundownView(self) self.mcr = self.plugins = False if has_right("mcr", anyval=True): self.mcr = MCR(self) self.plugins = PlayoutPlugins(self) if self.app_state.get("show_mcr", False): self.mcr.show() else: self.mcr.hide() if self.app_state.get("show_plugins", False): self.plugins.show() else: self.plugins.hide() layout.addWidget(self.mcr) layout.addWidget(self.plugins) layout.addWidget(self.view, 1) self.setLayout(layout)
def load(self, asset, **kwargs): id_folder = kwargs.get("id_folder", asset["id_folder"]) if id_folder != self.id_folder or kwargs.get("force"): if not id_folder: self.keys = [] else: self.keys = config["folders"][id_folder]["meta_set"] if self.form: # SRSLY. I've no idea what I'm doing here self.layout.removeWidget(self.form) self.form.deleteLater() QApplication.processEvents() self.form.destroy() QApplication.processEvents() self.form = None for i in reversed(range(self.layout.count())): self.layout.itemAt(i).widget().deleteLater() self.form = MetaEditor(self, self.keys) self.layout.addWidget(self.form) self.id_folder = id_folder self.status = asset["status"] if self.form: for key, conf in self.keys: if meta_types[key]["class"] in [ MetaClass.SELECT, MetaClass.LIST ]: self.form.inputs[key].set_data( asset.show(key, result="full")) self.form[key] = asset[key] self.form.set_defaults() if self.form: enabled = has_right("asset_edit", id_folder) self.form.setEnabled(enabled)
def contextMenuEvent(self, event): if not self.cursor_event: return menu = QMenu(self.parent()) menu.setStyleSheet(app_skin) self.calendar.selected_event = self.cursor_event action_open_rundown = QAction("Open rundown", self) action_open_rundown.triggered.connect(self.on_open_rundown) menu.addAction(action_open_rundown) action_edit_event = QAction("Event details", self) action_edit_event.triggered.connect(self.on_edit_event) menu.addAction(action_edit_event) if has_right("scheduler_edit", self.calendar.id_channel): menu.addSeparator() action_delete_event = QAction("Delete event", self) action_delete_event.triggered.connect(self.on_delete_event) menu.addAction(action_delete_event) menu.exec_(event.globalPos())
def dragEnterEvent(self, evt): if not has_right("scheduler_edit", self.calendar.id_channel): return if evt.mimeData().hasFormat("application/nx.asset"): d = evt.mimeData().data("application/nx.asset").data() d = json.loads(d.decode("ascii")) if len(d) != 1: evt.ignore() return asset = Asset(meta=d[0]) if not eval(self.calendar.playout_config["scheduler_accepts"]): evt.ignore() return self.calendar.dragging = asset self.calendar.drag_offset = (20 / self.sec_size ) # TODO: SOMETHING MORE CLEVER evt.accept() elif evt.mimeData().hasFormat("application/nx.event"): d = evt.mimeData().data("application/nx.event").data() d = json.loads(d.decode("ascii")) if len(d) != 1: evt.ignore() return event = Event(meta=d[0]) self.calendar.dragging = event evt.accept() else: evt.ignore() if self.calendar.drag_source: self.calendar.drag_source.drag_outside = False self.calendar.drag_source.update()
def rundown_toolbar(wnd): action_find = QAction("Search rundown", wnd) action_find.setShortcut("Ctrl+F") action_find.triggered.connect(wnd.find) wnd.addAction(action_find) action_find_next = QAction("Search rundown", wnd) action_find_next.setShortcut("F3") action_find_next.triggered.connect(wnd.find_next) wnd.addAction(action_find_next) toolbar = QToolBar(wnd) action_day_prev = QAction(QIcon(pixlib["previous"]), "&Previous day", wnd) action_day_prev.setShortcut("Alt+Left") action_day_prev.setStatusTip("Go to previous day") action_day_prev.triggered.connect(wnd.go_day_prev) toolbar.addAction(action_day_prev) action_now = QAction(QIcon(pixlib["now"]), "&Now", wnd) action_now.setStatusTip("Go to now") action_now.triggered.connect(wnd.go_now) toolbar.addAction(action_now) action_calendar = QAction(QIcon(pixlib["calendar"]), "&Calendar", wnd) action_calendar.setShortcut("Ctrl+D") action_calendar.setStatusTip("Open calendar") action_calendar.triggered.connect(wnd.show_calendar) toolbar.addAction(action_calendar) action_refresh = QAction(QIcon(pixlib["refresh"]), "&Refresh", wnd) action_refresh.setStatusTip("Refresh rundown") action_refresh.triggered.connect(wnd.load) toolbar.addAction(action_refresh) action_day_next = QAction(QIcon(pixlib["next"]), "&Next day", wnd) action_day_next.setShortcut("Alt+Right") action_day_next.setStatusTip("Go to next day") action_day_next.triggered.connect(wnd.go_day_next) toolbar.addAction(action_day_next) if has_right("rundown_edit", anyval=True): toolbar.addSeparator() for btn_config in ITEM_BUTTONS: toolbar.addWidget(ItemButton(wnd, btn_config)) toolbar.addSeparator() action_toggle_mcr = QAction(QIcon(pixlib["mcr"]), "&Playout controls", wnd) action_toggle_mcr.setStatusTip("Toggle playout controls") action_toggle_mcr.triggered.connect(wnd.toggle_mcr) toolbar.addAction(action_toggle_mcr) action_toggle_plugins = QAction(QIcon(pixlib["plugins"]), "&Plugins controls", wnd) action_toggle_plugins.setShortcut("F4") action_toggle_plugins.setStatusTip("Toggle plugins controls") action_toggle_plugins.triggered.connect(wnd.toggle_plugins) toolbar.addAction(action_toggle_plugins) toolbar.addWidget(ToolBarStretcher(wnd)) wnd.channel_display = ChannelDisplay() toolbar.addWidget(wnd.channel_display) return toolbar
def can_edit(self): return has_right("rundown_edit", self.id_channel)
def create_menu(wnd): menubar = wnd.menuBar() menu_file = menubar.addMenu("&File") action_new_asset = QAction("&New asset", wnd) action_new_asset.setShortcut("Ctrl+N") action_new_asset.setStatusTip("Create new asset from template") action_new_asset.triggered.connect(wnd.new_asset) action_new_asset.setEnabled( has_right("asset_create") and config.get("ui_asset_create", True) ) menu_file.addAction(action_new_asset) action_clone_asset = QAction("&Clone asset", wnd) action_clone_asset.setShortcut("Ctrl+Shift+N") action_clone_asset.setStatusTip("Clone current asset") action_clone_asset.triggered.connect(wnd.clone_asset) action_clone_asset.setEnabled( has_right("asset_create") and config.get("ui_asset_create", True) ) menu_file.addAction(action_clone_asset) menu_file.addSeparator() action_refresh = QAction("&Refresh", wnd) action_refresh.setShortcut("F5") action_refresh.setStatusTip("Refresh views") action_refresh.triggered.connect(wnd.refresh) menu_file.addAction(action_refresh) action_load_settings = QAction("&Reload settings", wnd) action_load_settings.setShortcut("Shift+F5") action_load_settings.setStatusTip("Reload system settings") action_load_settings.triggered.connect(wnd.load_settings) menu_file.addAction(action_load_settings) menu_file.addSeparator() action_logout = QAction("L&ogout", wnd) action_logout.setStatusTip("Log out user") action_logout.triggered.connect(wnd.logout) menu_file.addAction(action_logout) action_exit = QAction("E&xit", wnd) action_exit.setShortcut("Alt+F4") action_exit.setStatusTip("Quit Firefly") action_exit.triggered.connect(wnd.exit) menu_file.addAction(action_exit) # # Browser # menu_browser = menubar.addMenu("Browser") action_detail = QAction("Asset &detail", wnd) action_detail.setShortcut("F2") action_detail.setStatusTip("Focus asset search bar") action_detail.triggered.connect(wnd.show_detail) menu_browser.addAction(action_detail) action_search = QAction("&Search assets", wnd) action_search.setShortcut("ESC") action_search.setStatusTip("Focus asset search bar") action_search.triggered.connect(wnd.search_assets) menu_browser.addAction(action_search) menu_browser.addSeparator() action_new_tab = QAction("&New tab", wnd) action_new_tab.setShortcut("CTRL+T") action_new_tab.setStatusTip("Open new browser tab") action_new_tab.triggered.connect(wnd.new_tab) menu_browser.addAction(action_new_tab) action_close_tab = QAction("&Close tab", wnd) action_close_tab.setShortcut("CTRL+W") action_close_tab.setStatusTip("Close current browser tab") action_close_tab.triggered.connect(wnd.close_tab) menu_browser.addAction(action_close_tab) action_prev_tab = QAction("&Previous tab", wnd) action_prev_tab.setShortcut("CTRL+PgUp") action_prev_tab.triggered.connect(wnd.prev_tab) menu_browser.addAction(action_prev_tab) action_next_tab = QAction("&Next tab", wnd) action_next_tab.setShortcut("CTRL+PgDown") action_next_tab.triggered.connect(wnd.next_tab) menu_browser.addAction(action_next_tab) # # Scheduling # if config["playout_channels"]: wnd.menu_scheduler = menubar.addMenu("&Scheduler") ag = QActionGroup(wnd) ag.setExclusive(True) for id_channel in sorted(config["playout_channels"]): a = ag.addAction( QAction( config["playout_channels"][id_channel]["title"], wnd, checkable=True ) ) a.id_channel = id_channel a.triggered.connect(partial(wnd.set_channel, id_channel)) if ( has_right("rundown_view", a.id_channel) or has_right("rundown_edit", a.id_channel) or has_right("scheduler_view", a.id_channel) or has_right("scheduler_edit", a.id_channel) ): a.setEnabled(True) else: a.setEnabled(False) wnd.menu_scheduler.addAction(a) wnd.menu_scheduler.addSeparator() action_import_template = QAction("Import", wnd) action_import_template.setStatusTip("Import week template") action_import_template.triggered.connect(wnd.import_template) wnd.menu_scheduler.addAction(action_import_template) action_export_template = QAction("Export", wnd) action_export_template.setStatusTip("Export current week as template") action_export_template.triggered.connect(wnd.export_template) wnd.menu_scheduler.addAction(action_export_template) # # Rundown # menu_rundown = menubar.addMenu("&Rundown") action_now = QAction("Now", wnd) action_now.setShortcut("F1") action_now.setStatusTip("Open current position in rundown") action_now.setEnabled(has_right("rundown_view")) action_now.triggered.connect(wnd.now) menu_rundown.addAction(action_now) wnd.action_rundown_edit = QAction("Rundown edit mode", wnd) wnd.action_rundown_edit.setShortcut("Ctrl+R") wnd.action_rundown_edit.setStatusTip("Toggle rundown edit mode") wnd.action_rundown_edit.setCheckable(True) wnd.action_rundown_edit.setEnabled(has_right("rundown_edit")) wnd.action_rundown_edit.triggered.connect(wnd.toggle_rundown_edit) menu_rundown.addAction(wnd.action_rundown_edit) menu_rundown.addSeparator() action_refresh_plugins = QAction("Refresh plugins", wnd) action_refresh_plugins.setStatusTip("Refresh rundown plugins") action_refresh_plugins.triggered.connect(wnd.refresh_plugins) menu_rundown.addAction(action_refresh_plugins) # # HELP # menu_help = menubar.addMenu("Help") wnd.action_debug = QAction("Debug mode", wnd) wnd.action_debug.setStatusTip("Toggle debug mode") wnd.action_debug.setCheckable(True) wnd.action_debug.setChecked(config.get("debug", False)) wnd.action_debug.triggered.connect(wnd.toggle_debug_mode) menu_help.addAction(wnd.action_debug) menu_help.addSeparator() action_about = QAction("&About", wnd) action_about.setStatusTip("About Firefly") action_about.triggered.connect(partial(show_about_dialog, wnd)) menu_help.addAction(action_about)
def can_schedule(self): return has_right("scheduler_edit", self.id_channel)
def dropEvent(self, evt): drop_ts = max( self.start_time, self.round_ts(self.cursor_time - self.calendar.drag_offset)) do_reload = False if not has_right("scheduler_edit", self.id_channel): logging.error( "You are not allowed to modify schedule of this channel.") self.calendar.drag_source = False self.calendar.dragging = False return elif type(self.calendar.dragging) == Asset: for event in self.calendar.events: if event["start"] == drop_ts: if event["duration"]: ret = QMessageBox.question( self, "Overwrite", f"Do you really want to overwrite {event}", QMessageBox.Yes | QMessageBox.No, ) if ret == QMessageBox.Yes: pass else: self.calendar.drag_source = False self.calendar.dragging = False self.update() return if evt.keyboardModifiers() & Qt.AltModifier: logging.info(f"Creating event from {self.calendar.dragging}" f"at time {format_time(self.cursor_time)}") if show_event_dialog( asset=self.calendar.dragging, id_channel=self.id_channel, start=drop_ts, ): do_reload = True else: self.calendar.setCursor(Qt.WaitCursor) response = api.schedule( id_channel=self.id_channel, start_time=self.calendar.week_start_time, end_time=self.calendar.week_end_time, events=[{ "id_asset": self.calendar.dragging.id, "start": drop_ts, "id_channel": self.id_channel, }], ) self.calendar.setCursor(Qt.ArrowCursor) if not response: logging.error(response.message) do_reload = True elif type(self.calendar.dragging) == Event: event = self.calendar.dragging move = True if event.id and abs(event["start"] - drop_ts) > 7200: ret = QMessageBox.question( self, "Move event", f"Do you really want to move {self.cursor_event}?" f"\n\nFrom: {format_time(event['start'])}" f"\nTo: {format_time(drop_ts)}", QMessageBox.Yes | QMessageBox.No, ) if ret == QMessageBox.Yes: move = True else: move = False if move: event["start"] = drop_ts if not event.id: logging.debug("Creating empty event") # Create empty event. Event edit dialog is enforced. if show_event_dialog(id_channel=self.id_channel, start=drop_ts): do_reload = True else: # Just dragging events around. Instant save self.calendar.setCursor(Qt.ArrowCursor) response = api.schedule( id_channel=self.id_channel, start_time=self.calendar.week_start_time, end_time=self.calendar.week_end_time, events=[event.meta], ) self.calendar.setCursor(Qt.ArrowCursor) if not response: logging.error(response.message) else: do_reload = response.data self.calendar.drag_source = False self.calendar.dragging = False if type(do_reload) == list: self.calendar.set_data(do_reload) elif do_reload: self.calendar.load()