From 653b25666da4dc5ff4f6ac2ff05c75d1ab47b74a Mon Sep 17 00:00:00 2001 From: HarishFulara07 Date: Tue, 5 Jun 2018 00:46:10 +0530 Subject: [PATCH] ui: welcome window integration --- data/ui/greeter.ui | 127 ++ data/ui/mainmenubutton.ui | 28 +- pitivi/application.py | 41 +- pitivi/clipproperties.py | 6 +- pitivi/dialogs/about.py | 108 ++ pitivi/dialogs/browseprojects.py | 68 + pitivi/dialogs/startupwizard.py | 167 --- pitivi/editorperspective.py | 1076 ++++++++++++++ pitivi/effects.py | 2 +- pitivi/greeterperspective.py | 228 +++ pitivi/mainwindow.py | 1295 ++--------------- pitivi/medialibrary.py | 10 +- pitivi/perspective.py | 39 + pitivi/project.py | 14 +- pitivi/render.py | 2 +- pitivi/timeline/elements.py | 8 +- pitivi/timeline/ruler.py | 2 +- pitivi/timeline/timeline.py | 6 +- pitivi/titleeditor.py | 6 +- pitivi/utils/pipeline.py | 2 +- pitivi/utils/timeline.py | 9 +- pitivi/utils/ui.py | 26 + pitivi/utils/widgets.py | 24 +- pitivi/viewer/overlay.py | 4 +- pitivi/viewer/viewer.py | 27 +- pre-commit.hook | 2 +- ...ainwindow.py => test_editorperspective.py} | 43 +- tests/test_project.py | 32 +- 28 files changed, 1869 insertions(+), 1533 deletions(-) create mode 100644 data/ui/greeter.ui create mode 100644 pitivi/dialogs/about.py create mode 100644 pitivi/dialogs/browseprojects.py delete mode 100644 pitivi/dialogs/startupwizard.py create mode 100644 pitivi/editorperspective.py create mode 100644 pitivi/greeterperspective.py create mode 100644 pitivi/perspective.py rename tests/{test_mainwindow.py => test_editorperspective.py} (76%) diff --git a/data/ui/greeter.ui b/data/ui/greeter.ui new file mode 100644 index 000000000..5cbb39496 --- /dev/null +++ b/data/ui/greeter.ui @@ -0,0 +1,127 @@ + + + + + + True + True + never + + + True + False + + + True + False + vertical + + + True + False + True + + + False + 6 + end + + + + + + + + + + + + False + False + 0 + + + + + False + 16 + + + + + + + + + + + + False + False + 0 + + + + + False + True + 0 + + + + + True + False + center + 30 + 10 + vertical + + + recent_projects_labelbox + True + False + + + True + False + Recent Projects + + + False + True + 0 + + + + + False + True + 0 + + + + + recent_projects_listbox + True + False + + + False + True + 2 + + + + + False + True + 2 + + + + + + + + diff --git a/data/ui/mainmenubutton.ui b/data/ui/mainmenubutton.ui index c42905ef8..4943108eb 100644 --- a/data/ui/mainmenubutton.ui +++ b/data/ui/mainmenubutton.ui @@ -6,36 +6,12 @@ True False end - - - True - False - win.new-project - New project - True - - - - - True - False - win.open-project - Open project... - True - - - - - True - False - - True False Save the current project under a new name or a different location - win.save-as + editor.save-as Save As... True @@ -129,9 +105,9 @@ True False + win.about About True - diff --git a/pitivi/application.py b/pitivi/application.py index b855be0a7..ee47959a0 100644 --- a/pitivi/application.py +++ b/pitivi/application.py @@ -29,7 +29,6 @@ from gi.repository import Gtk from pitivi.configure import RELEASES_URL from pitivi.configure import VERSION -from pitivi.dialogs.startupwizard import StartUpWizard from pitivi.effects import EffectsManager from pitivi.mainwindow import MainWindow from pitivi.pluginmanager import PluginManager @@ -58,6 +57,7 @@ class Pitivi(Gtk.Application, Loggable): action_log (UndoableActionLog): The undo/redo log for the current project. effects (EffectsManager): The effects which can be applied to a clip. gui (MainWindow): The main window of the app. + recent_manager (Gtk.RecentManager): Manages recently used projects. project_manager (ProjectManager): The holder of the current project. settings (GlobalSettings): The application-wide settings. system (pitivi.utils.system.System): The system running the app. @@ -84,7 +84,7 @@ class Pitivi(Gtk.Application, Loggable): self._last_action_time = Gst.util_get_timestamp() self.gui = None - self.__welcome_wizard = None + self.recent_manager = Gtk.RecentManager.get_default() self.__inhibit_cookies = {} self._version_information = {} @@ -152,6 +152,7 @@ class Pitivi(Gtk.Application, Loggable): self.project_manager.connect( "new-project-loaded", self._newProjectLoaded) self.project_manager.connect("project-closed", self._projectClosed) + self.project_manager.connect("project-saved", self.__project_saved_cb) self._createActions() self._syncDoUndo() @@ -198,27 +199,21 @@ class Pitivi(Gtk.Application, Loggable): self.gui.present() # No need to show the welcome wizard. return - self.createMainWindow() - self.welcome_wizard.show() - - @property - def welcome_wizard(self): - if not self.__welcome_wizard: - self.__welcome_wizard = StartUpWizard(self) - return self.__welcome_wizard + self.create_main_window() + self.gui.show_perspective(self.gui.greeter) + self.gui.show() - def createMainWindow(self): + def create_main_window(self): if self.gui: return self.gui = MainWindow(self) + self.gui.setup_ui() self.add_window(self.gui) - self.gui.checkScreenConstraints() - # We might as well show it. - self.gui.show() def do_open(self, giofiles, unused_count, unused_hint): assert giofiles - self.createMainWindow() + self.create_main_window() + self.gui.show() if len(giofiles) > 1: self.warning( "Can open only one project file at a time. Ignoring the rest!") @@ -238,8 +233,6 @@ class Pitivi(Gtk.Application, Loggable): self.warning( "Not closing since running project doesn't want to close") return False - if self.welcome_wizard: - self.welcome_wizard.hide() if self.gui: self.gui.destroy() self.threads.stopAllThreads() @@ -276,13 +269,23 @@ class Pitivi(Gtk.Application, Loggable): self._setScenarioFile(project.get_uri()) def _newProjectLoaded(self, unused_project_manager, project): + uri = project.get_uri() + if uri: + # We remove the project from recent projects list + # and then re-add it to this list to make sure it + # gets positioned at the top of the recent projects list. + self.recent_manager.remove_item(uri) + self.recent_manager.add_item(uri) self.action_log = UndoableActionLog() self.action_log.connect("pre-push", self._action_log_pre_push_cb) self.action_log.connect("commit", self._actionLogCommit) self.action_log.connect("move", self._action_log_move_cb) - self.project_observer = ProjectObserver(project, self.action_log) + def __project_saved_cb(self, unused_project_manager, unused_project, uri): + if uri: + self.recent_manager.add_item(uri) + def _projectClosed(self, unused_project_manager, project): if project.loaded: self.action_log = None @@ -389,7 +392,7 @@ class Pitivi(Gtk.Application, Loggable): self.project_manager.current_project.setModificationState(dirty) # In the tests we do not want to create any gui if self.gui is not None: - self.gui.showProjectStatus() + self.gui.editor.showProjectStatus() def simple_inhibit(self, reason, flags): """Informs the session manager about actions to be inhibited. diff --git a/pitivi/clipproperties.py b/pitivi/clipproperties.py index 84f1f9434..3a34cb9a2 100644 --- a/pitivi/clipproperties.py +++ b/pitivi/clipproperties.py @@ -785,7 +785,7 @@ class TransformationProperties(Gtk.Expander, Loggable): for prop in ["posx", "posy", "width", "height"]: self.__update_spin_btn(prop) # Keep the overlay stack in sync with the spin buttons values - self.app.gui.viewer.overlay_stack.update(self.source) + self.app.gui.editor.viewer.overlay_stack.update(self.source) def __source_property_changed_cb(self, unused_source, unused_element, param): self.__update_spin_btn(param.name) @@ -862,7 +862,7 @@ class TransformationProperties(Gtk.Expander, Loggable): if value != cvalue: self.__set_prop(prop, value) - self.app.gui.viewer.overlay_stack.update(self.source) + self.app.gui.editor.viewer.overlay_stack.update(self.source) def __set_source(self, source): if self.source: @@ -888,7 +888,7 @@ class TransformationProperties(Gtk.Expander, Loggable): if source: self._selected_clip = clip self.__set_source(source) - self.app.gui.viewer.overlay_stack.select(source) + self.app.gui.editor.viewer.overlay_stack.select(source) self.show() return diff --git a/pitivi/dialogs/about.py b/pitivi/dialogs/about.py new file mode 100644 index 000000000..3ac29ecf5 --- /dev/null +++ b/pitivi/dialogs/about.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Pitivi video editor +# Copyright (c) 2005, Edward Hervey +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +"""Pitivi's about dialog.""" +from gettext import gettext as _ + +from gi.repository import GES +from gi.repository import Gst +from gi.repository import Gtk + +from pitivi.configure import APPNAME +from pitivi.configure import APPURL +from pitivi.configure import GITVERSION +from pitivi.configure import in_devel +from pitivi.configure import VERSION + + +# pylint: disable=too-few-public-methods +class AboutDialog(Gtk.AboutDialog): + """Pitivi's about dialog. + + Displays info regarding Pitivi's version, license, + maintainers, contributors, etc. + + Attributes: + app (Pitivi): The app. + """ + + def __init__(self, app): + Gtk.AboutDialog.__init__(self) + self.set_program_name(APPNAME) + self.set_website(APPURL) + + if in_devel(): + version_str = _("Development version: %s") % GITVERSION + elif not app.isLatest(): + version_str = _("Version %(cur_ver)s — %(new_ver)s is available") % \ + {"cur_ver": GITVERSION, + "new_ver": app.getLatest()} + elif GITVERSION: + version_str = _("Version %s") % GITVERSION + else: + version_str = _("Version %s") % VERSION + self.set_version(version_str) + + comments = ["", + "GES %s" % ".".join(map(str, GES.version())), + "GTK+ %s" % ".".join(map(str, (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION))), + "GStreamer %s" % ".".join(map(str, Gst.version()))] + self.set_comments("\n".join(comments)) + + authors = [_("Current maintainers:"), + "Jean-François Fortin Tam ", + "Thibault Saunier ", + "Mathieu Duponchelle ", + "Alexandru Băluț ", + "", + _("Past maintainers:"), + "Edward Hervey ", + "Alessandro Decina ", + "Brandon Lewis ", + "", + # Translators: this paragraph is to be translated, the list + # of contributors is shown dynamically as a clickable link + # below it + _("Contributors:\n" + + "A handwritten list here would...\n" + + "• be too long,\n" + + "• be frequently outdated,\n" + + "• not show their relative merit.\n\n" + + "Out of respect for our contributors, we point you instead to:\n"), + # Translators: keep the %s at the end of the 1st line + _("The list of contributors on Ohloh %s\n" + + "Or you can run: git shortlog -s -n") + % "http://ohloh.net/p/pitivi/contributors", ] + self.set_authors(authors) + # Translators: See + # https://developer.gnome.org/gtk3/stable/GtkAboutDialog.html#gtk-about-dialog-set-translator-credits + # for details on how this is used. + translators = _("translator-credits") + if translators != "translator-credits": + self.set_translator_credits(translators) + documenters = ["Jean-François Fortin Tam ", ] + self.set_documenters(documenters) + self.set_license_type(Gtk.License.LGPL_2_1) + self.set_icon_name("pitivi") + self.set_logo_icon_name("pitivi") + self.connect("response", self.__about_response_cb) + self.set_transient_for(app.gui) + + @staticmethod + def __about_response_cb(dialog, unused_response): + dialog.destroy() diff --git a/pitivi/dialogs/browseprojects.py b/pitivi/dialogs/browseprojects.py new file mode 100644 index 000000000..3815813b6 --- /dev/null +++ b/pitivi/dialogs/browseprojects.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Pitivi video editor +# Copyright (c) 2005, Edward Hervey +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +"""Dialog for browsing through projects to open an existing project.""" +from gettext import gettext as _ + +from gi.repository import GES +from gi.repository import Gtk + + +# pylint: disable=too-few-public-methods +class BrowseProjectsDialog(Gtk.FileChooserDialog): + """Displays the Gtk.FileChooserDialog for browsing projects. + + Attributes: + app (Pitivi): The app. + """ + + def __init__(self, app): + Gtk.FileChooserDialog.__init__(self) + + self.set_title(_("Open File...")) + self.set_transient_for(app.gui) + self.set_action(Gtk.FileChooserAction.OPEN) + + self.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, + _("Open"), Gtk.ResponseType.OK) + self.set_default_response(Gtk.ResponseType.OK) + self.set_select_multiple(False) + # TODO: Remove this set_current_folder call when GTK bug 683999 is + # fixed + self.set_current_folder(app.settings.lastProjectFolder) + formatter_assets = GES.list_assets(GES.Formatter) + formatter_assets.sort( + key=lambda x: - x.get_meta(GES.META_FORMATTER_RANK)) + for format_ in formatter_assets: + filt = Gtk.FileFilter() + filt.set_name(format_.get_meta(GES.META_DESCRIPTION)) + filt.add_pattern("*%s" % + format_.get_meta(GES.META_FORMATTER_EXTENSION)) + self.add_filter(filt) + default = Gtk.FileFilter() + default.set_name(_("All supported formats")) + default.add_custom(Gtk.FileFilterFlags.URI, self.__can_load_uri, None) + self.add_filter(default) + + # pylint: disable=bare-except + @staticmethod + def __can_load_uri(filterinfo, unused_uri): + try: + return GES.Formatter.can_load_uri(filterinfo.uri) + except: + return False diff --git a/pitivi/dialogs/startupwizard.py b/pitivi/dialogs/startupwizard.py deleted file mode 100644 index 74679d2d9..000000000 --- a/pitivi/dialogs/startupwizard.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# Pitivi video editor -# Copyright (c) 2010 Mathieu Duponchelle -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, -# Boston, MA 02110-1301, USA. -import os -from gettext import gettext as _ - -from gi.repository import Gdk -from gi.repository import GES -from gi.repository import Gtk - -from pitivi.check import missing_soft_deps -from pitivi.configure import get_ui_dir -from pitivi.dialogs.depsmanager import DepsManager -from pitivi.shortcuts import show_shortcuts -from pitivi.utils.misc import show_user_manual -from pitivi.utils.ui import fix_infobar - - -class StartUpWizard(object): - """A Wizard displaying recent projects. - - Allows the user to: - - create a new project and open the settings dialog (Create button), - - create a new project with the default settings (dialog close or ESC), - - load a recently opened project (double click recent chooser), - - load a project (Browse button), - - see the quick start manual (User Manual button). - """ - - @staticmethod - def _userManualCb(unused_button): - """Handles a click on the Help button.""" - show_user_manual() - - def _cheatsheetCb(self, unused_button): - """Shows the shortcuts cheatsheet.""" - show_shortcuts(self.app) - - def __init__(self, app): - self.app = app - self.builder = Gtk.Builder() - self.builder.add_from_file( - os.path.join(get_ui_dir(), "startupwizard.ui")) - self.builder.connect_signals(self) - - self.window = self.builder.get_object("window1") - # The line below is supremely important, it will NOT work if set - # only by the GtkBuilder file. The DIALOG TypeHint allows proper - # attachment (visually, behaviorally) to MainWindow, and - # prevents other windows from showing on top too easily. - self.window.set_type_hint(Gdk.WindowTypeHint.DIALOG) - - self.recent_chooser = self.builder.get_object("recentchooser2") - # FIXME: gtk creates a combo box with only one item, but there is no - # simple way to hide it. - _filter = Gtk.RecentFilter() - _filter.set_name(_("Projects")) - - for asset in GES.list_assets(GES.Formatter): - _filter.add_pattern( - '*.' + asset.get_meta(GES.META_FORMATTER_EXTENSION)) - - self.recent_chooser.add_filter(_filter) - - missing_button = self.builder.get_object("missing_deps_button") - - if not missing_soft_deps: - missing_button.hide() - - vbox = self.builder.get_object("topvbox") - self.infobar = Gtk.InfoBar() - fix_infobar(self.infobar) - vbox.pack_start(self.infobar, True, True, 0) - if self.app.getLatest(): - self._appVersionInfoReceivedCb(self.app, None) - else: - self.app.connect( - "version-info-received", self._appVersionInfoReceivedCb) - - def _newProjectCb(self, unused_button): - """Handles a click on the New (Project) button.""" - self.app.project_manager.newBlankProject() - - def _loadCb(self, unused_recent_chooser): - """Handles choosing a project on the recent chooser. - - This calls the project manager to load the associated URI. - """ - uri = self.recent_chooser.get_current_uri() - self.app.project_manager.loadProject(uri) - - def _keyPressCb(self, unused_widget, event): - """Handles a key press event on the dialog.""" - if event.keyval == Gdk.KEY_Escape: - # The user pressed "Esc". - self.app.project_manager.newBlankProject() - - def _onBrowseButtonClickedCb(self, unused_button6): - """Handles a click on the Browse button.""" - self.app.gui.openProject() - - def _onMissingDepsButtonClickedCb(self, unused_button): - """Handles a click on the Missing Deps button.""" - DepsManager(self.app, parent_window=self.window) - - def _deleteCb(self, unused_widget, unused_event): - """Handles a click on the X button of the dialog.""" - self.app.project_manager.newBlankProject() - return True - - def show(self): - if self.window.props.visible: - return - self.window.set_transient_for(self.app.gui) - self.window.show() - project_manager = self.app.project_manager - project_manager.connect("new-project-loading", self._projectLoadingCb) - - def hide(self): - if not self.window.props.visible: - return - self.window.hide() - project_manager = self.app.project_manager - project_manager.disconnect_by_func(self._projectLoadingCb) - - def _projectLoadingCb(self, unused_project_manager, unused_uri): - """Handles the start of a project load operation.""" - self.hide() - - def _appVersionInfoReceivedCb(self, app, unused_version_information): - """Handles new version info.""" - if app.isLatest(): - # current version, don't show message - return - - latest_version = app.getLatest() - if self.app.settings.lastCurrentVersion != latest_version: - # new latest version, reset counter - self.app.settings.lastCurrentVersion = latest_version - self.app.settings.displayCounter = 0 - - if self.app.settings.displayCounter >= 5: - # current version info already showed 5 times, don't show again - return - - # increment counter, create infobar and show info - self.app.settings.displayCounter = self.app.settings.displayCounter + 1 - text = _("Pitivi %s is available.") % latest_version - label = Gtk.Label(label=text) - self.infobar.get_content_area().add(label) - self.infobar.set_message_type(Gtk.MessageType.INFO) - self.infobar.show_all() diff --git a/pitivi/editorperspective.py b/pitivi/editorperspective.py new file mode 100644 index 000000000..b263e7c45 --- /dev/null +++ b/pitivi/editorperspective.py @@ -0,0 +1,1076 @@ +# -*- coding: utf-8 -*- +# Pitivi video editor +# Copyright (c) 2005, Edward Hervey +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +import os +from gettext import gettext as _ +from time import time +from urllib.parse import unquote + +from gi.repository import Gdk +from gi.repository import GES +from gi.repository import Gio +from gi.repository import GstPbutils +from gi.repository import Gtk + +from pitivi.clipproperties import ClipProperties +from pitivi.configure import APPNAME +from pitivi.configure import get_ui_dir +from pitivi.dialogs.prefs import PreferencesDialog +from pitivi.effects import EffectListWidget +from pitivi.mediafilespreviewer import PreviewWidget +from pitivi.medialibrary import AssetThumbnail +from pitivi.medialibrary import MediaLibraryWidget +from pitivi.perspective import Perspective +from pitivi.project import ProjectSettingsDialog +from pitivi.settings import GlobalSettings +from pitivi.tabsmanager import BaseTabs +from pitivi.timeline.timeline import TimelineContainer +from pitivi.titleeditor import TitleEditor +from pitivi.transitions import TransitionsListWidget +from pitivi.utils.loggable import Loggable +from pitivi.utils.misc import path_from_uri +from pitivi.utils.ui import beautify_missing_asset +from pitivi.utils.ui import beautify_time_delta +from pitivi.utils.ui import clear_styles +from pitivi.utils.ui import info_name +from pitivi.utils.ui import PADDING +from pitivi.utils.ui import SPACING +from pitivi.utils.ui import TIMELINE_CSS +from pitivi.viewer.viewer import ViewerContainer + + +GlobalSettings.addConfigSection("main-window") +GlobalSettings.addConfigOption('mainWindowHPanePosition', + section="main-window", + key="hpane-position", + type_=int) +GlobalSettings.addConfigOption('mainWindowMainHPanePosition', + section="main-window", + key="main-hpane-position", + type_=int) +GlobalSettings.addConfigOption('mainWindowVPanePosition', + section="main-window", + key="vpane-position", + type_=int) +GlobalSettings.addConfigOption('lastProjectFolder', + section="main-window", + key="last-folder", + environment="PITIVI_PROJECT_FOLDER", + default=os.path.expanduser("~")) + + +class EditorPerspective(Perspective, Loggable): + """Pitivi's Editor perspective. + + Attributes: + app (Pitivi): The app. + """ + + def __init__(self, app): + Perspective.__init__(self) + Loggable.__init__(self) + + self.app = app + self.settings = app.settings + + self.builder_handler_ids = [] + self.builder = Gtk.Builder() + + pm = self.app.project_manager + pm.connect("new-project-loaded", + self._projectManagerNewProjectLoadedCb) + pm.connect("save-project-failed", + self._projectManagerSaveProjectFailedCb) + pm.connect("project-saved", self._projectManagerProjectSavedCb) + pm.connect("closing-project", self._projectManagerClosingProjectCb) + pm.connect("reverting-to-saved", + self._projectManagerRevertingToSavedCb) + pm.connect("project-closed", self._projectManagerProjectClosedCb) + pm.connect("missing-uri", self._projectManagerMissingUriCb) + + def setup_ui(self): + """Sets up the UI.""" + self.__setup_css() + self._createUi() + self.app.gui.connect("destroy", self._destroyedCb) + + def refresh(self): + """Refreshes the perspective.""" + self.focusTimeline() + + def __setup_css(self): + css_provider = Gtk.CssProvider() + css_provider.load_from_data(TIMELINE_CSS.encode('UTF-8')) + screen = Gdk.Screen.get_default() + style_context = self.app.gui.get_style_context() + style_context.add_provider_for_screen(screen, css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + def _destroyedCb(self, unused_main_window): + """Cleanup before destroying this window.""" + pm = self.app.project_manager + pm.disconnect_by_func(self._projectManagerNewProjectLoadedCb) + pm.disconnect_by_func(self._projectManagerSaveProjectFailedCb) + pm.disconnect_by_func(self._projectManagerProjectSavedCb) + pm.disconnect_by_func(self._projectManagerClosingProjectCb) + pm.disconnect_by_func(self._projectManagerRevertingToSavedCb) + pm.disconnect_by_func(self._projectManagerProjectClosedCb) + pm.disconnect_by_func(self._projectManagerMissingUriCb) + self.toplevel_widget.remove(self.timeline_ui) + self.timeline_ui.destroy() + + def _renderCb(self, unused_button): + """Shows the RenderDialog for the current project.""" + from pitivi.render import RenderDialog + + project = self.app.project_manager.current_project + dialog = RenderDialog(self.app, project) + dialog.window.show() + + def _createUi(self): + """Creates the graphical interface. + + The rough hierarchy is: + vpaned: + - mainhpaned(secondhpaned(main_tabs, context_tabs), viewer) + - timeline_ui + + The full hierarchy can be admired by starting the GTK+ Inspector + with Ctrl+Shift+I. + """ + # Main "toolbar" (using client-side window decorations with HeaderBar) + self.headerbar = self.__create_headerbar() + + # Set up our main containers, in the order documented above + + # Separates the tabs+viewer from the timeline + self.toplevel_widget = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) + # Separates the tabs from the viewer + self.mainhpaned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) + # Separates the two sets of tabs + self.secondhpaned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) + self.toplevel_widget.pack1(self.mainhpaned, resize=False, shrink=False) + self.mainhpaned.pack1(self.secondhpaned, resize=True, shrink=False) + self.toplevel_widget.show() + self.secondhpaned.show() + self.mainhpaned.show() + + # First set of tabs + self.main_tabs = BaseTabs(self.app) + self.medialibrary = MediaLibraryWidget(self.app) + self.effectlist = EffectListWidget(self.app) + self.main_tabs.append_page("Media Library", + self.medialibrary, Gtk.Label(label=_("Media Library"))) + self.main_tabs.append_page("Effect Library", + self.effectlist, Gtk.Label(label=_("Effect Library"))) + self.medialibrary.connect('play', self._mediaLibraryPlayCb) + self.medialibrary.show() + self.effectlist.show() + + # Second set of tabs + self.context_tabs = BaseTabs(self.app) + self.clipconfig = ClipProperties(self.app) + self.trans_list = TransitionsListWidget(self.app) + self.title_editor = TitleEditor(self.app) + self.context_tabs.append_page("Clip", + self.clipconfig, Gtk.Label(label=_("Clip"))) + self.context_tabs.append_page("Transition", + self.trans_list, Gtk.Label(label=_("Transition"))) + self.context_tabs.append_page("Title", + self.title_editor.widget, Gtk.Label(label=_("Title"))) + # Show by default the Title tab, as the Clip and Transition tabs + # are useful only when a clip or transition is selected, but + # the Title tab allows adding titles. + self.context_tabs.set_current_page(2) + + self.secondhpaned.pack1(self.main_tabs, resize=False, shrink=False) + self.secondhpaned.pack2(self.context_tabs, resize=False, shrink=False) + self.main_tabs.show() + self.context_tabs.show() + + # Viewer + self.viewer = ViewerContainer(self.app) + self.mainhpaned.pack2(self.viewer, resize=True, shrink=False) + + # Now, the lower part: the timeline + self.timeline_ui = TimelineContainer(self.app) + self.toplevel_widget.pack2(self.timeline_ui, resize=True, shrink=False) + + # Setup shortcuts for HeaderBar buttons and menu items. + self.__set_keyboard_shortcuts() + + # Identify widgets for AT-SPI, making our test suite easier to develop + # These will show up in sniff, accerciser, etc. + self.headerbar.get_accessible().set_name("editor_headerbar") + self.menu_button.get_accessible().set_name("main menu button") + self.toplevel_widget.get_accessible().set_name("contents") + self.mainhpaned.get_accessible().set_name("upper half") + self.secondhpaned.get_accessible().set_name("tabs") + self.main_tabs.get_accessible().set_name("primary tabs") + self.context_tabs.get_accessible().set_name("secondary tabs") + self.viewer.get_accessible().set_name("viewer") + self.timeline_ui.get_accessible().set_name("timeline area") + + # Restore settings for position and visibility. + if self.settings.mainWindowHPanePosition is None: + self._setDefaultPositions() + self.secondhpaned.set_position(self.settings.mainWindowHPanePosition) + self.mainhpaned.set_position(self.settings.mainWindowMainHPanePosition) + self.toplevel_widget.set_position(self.settings.mainWindowVPanePosition) + + self.updateTitle() + + def _setDefaultPositions(self): + window_width = self.app.gui.get_size()[0] + if self.settings.mainWindowHPanePosition is None: + self.settings.mainWindowHPanePosition = window_width / 3 + if self.settings.mainWindowMainHPanePosition is None: + self.settings.mainWindowMainHPanePosition = 2 * window_width / 3 + if self.settings.mainWindowVPanePosition is None: + screen_width = float(self.app.gui.get_screen().get_width()) + screen_height = float(self.app.gui.get_screen().get_height()) + req = self.toplevel_widget.get_preferred_size()[0] + if screen_width / screen_height < 0.75: + # Tall screen, give some more vertical space the the tabs. + value = req.height / 3 + else: + value = req.height / 2 + self.settings.mainWindowVPanePosition = value + + def switchContextTab(self, ges_clip): + """Activates the appropriate tab on the second set of tabs. + + Args: + ges_clip (GES.SourceClip): The clip which has been focused. + """ + if isinstance(ges_clip, GES.TitleClip): + page = 2 + elif isinstance(ges_clip, GES.SourceClip): + page = 0 + elif isinstance(ges_clip, GES.TransitionClip): + page = 1 + else: + self.warning("Unknown clip type: %s", ges_clip) + return + self.context_tabs.set_current_page(page) + + def focusTimeline(self): + layers_representation = self.timeline_ui.timeline.layout + # Check whether it has focus already, grab_focus always emits an event. + if not layers_representation.props.is_focus: + layers_representation.grab_focus() + + def __create_headerbar(self): + headerbar = Gtk.HeaderBar() + headerbar.set_show_close_button(True) + + back_button = Gtk.Button.new_from_icon_name( + "go-previous-symbolic", Gtk.IconSize.SMALL_TOOLBAR) + back_button.set_always_show_image(True) + back_button.set_tooltip_text(_("Close project")) + back_button.connect("clicked", self.__close_project_cb) + back_button.set_margin_right(4 * PADDING) + headerbar.pack_start(back_button) + + undo_button = Gtk.Button.new_from_icon_name( + "edit-undo-symbolic", Gtk.IconSize.SMALL_TOOLBAR) + undo_button.set_always_show_image(True) + undo_button.set_label(_("Undo")) + undo_button.set_action_name("app.undo") + undo_button.set_use_underline(True) + + redo_button = Gtk.Button.new_from_icon_name( + "edit-redo-symbolic", Gtk.IconSize.SMALL_TOOLBAR) + redo_button.set_always_show_image(True) + redo_button.set_action_name("app.redo") + redo_button.set_use_underline(True) + + self.save_button = Gtk.Button.new_with_label(_("Save")) + self.save_button.set_focus_on_click(False) + + self.render_button = Gtk.Button.new_from_icon_name( + "system-run-symbolic", Gtk.IconSize.SMALL_TOOLBAR) + self.render_button.set_always_show_image(True) + self.render_button.set_label(_("Render")) + self.render_button.set_tooltip_text( + _("Export your project as a finished movie")) + self.render_button.set_sensitive(False) # The only one we have to set. + self.render_button.connect("clicked", self._renderCb) + + undo_redo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) + undo_redo_box.get_style_context().add_class("linked") + undo_redo_box.pack_start(undo_button, expand=False, fill=False, padding=0) + undo_redo_box.pack_start(redo_button, expand=False, fill=False, padding=0) + headerbar.pack_start(undo_redo_box) + + self.builder.add_from_file( + os.path.join(get_ui_dir(), "mainmenubutton.ui")) + + # FIXME : see https://bugzilla.gnome.org/show_bug.cgi?id=729263 + self.builder.connect_signals_full(self._builderConnectCb, self) + + self.menu_button = self.builder.get_object("menubutton") + + self._menubutton_items = {} + for widget in self.builder.get_object("menu").get_children(): + self._menubutton_items[Gtk.Buildable.get_name(widget)] = widget + + headerbar.pack_end(self.menu_button) + headerbar.pack_end(self.save_button) + headerbar.pack_end(self.render_button) + headerbar.show_all() + + return headerbar + + def __set_keyboard_shortcuts(self): + group = Gio.SimpleActionGroup() + self.toplevel_widget.insert_action_group("editor", group) + self.headerbar.insert_action_group("editor", group) + + self.save_action = Gio.SimpleAction.new("save", None) + self.save_action.connect("activate", self._saveProjectCb) + group.add_action(self.save_action) + self.app.shortcuts.add("editor.save", ["s"], + _("Save the current project"), group="win") + self.save_button.set_action_name("editor.save") + + self.save_as_action = Gio.SimpleAction.new("save-as", None) + self.save_as_action.connect("activate", self._saveProjectAsCb) + group.add_action(self.save_as_action) + self.app.shortcuts.add("editor.save-as", ["s"], + _("Save the current project as"), group="win") + + self.import_asset_action = Gio.SimpleAction.new("import-asset", None) + self.import_asset_action.connect("activate", self.__import_asset_cb) + group.add_action(self.import_asset_action) + self.app.shortcuts.add("editor.import-asset", ["i"], + _("Add media files to your project"), group="win") + + def __import_asset_cb(self, unusdaction, unusedparam): + self.medialibrary.show_import_assets_dialog() + + def showProjectStatus(self): + project = self.app.project_manager.current_project + dirty = project.hasUnsavedModifications() + self.save_action.set_enabled(dirty) + if project.uri: + self._menubutton_items["menu_revert_to_saved"].set_sensitive(dirty) + self.updateTitle() + +# UI Callbacks + + def _mediaLibraryPlayCb(self, unused_medialibrary, asset): + """Previews the specified asset. + + If the media library item to preview is an image, show it in the user's + favorite image viewer. Else, preview the video/sound in Pitivi. + """ + # Technically, our preview widget can show images, but it's never going + # to do a better job (sizing, zooming, metadata, editing, etc.) + # than the user's favorite image viewer. + if asset.is_image(): + Gio.AppInfo.launch_default_for_uri(asset.get_id(), None) + else: + preview_window = PreviewAssetWindow(asset, self.app) + preview_window.preview() + + def _projectChangedCb(self, unused_project): + self.save_action.set_enabled(True) + self.updateTitle() + + def _builderConnectCb(self, builder, gobject, signal_name, handler_name, + connect_object, flags, user_data): + id_ = gobject.connect(signal_name, getattr(self, handler_name)) + self.builder_handler_ids.append((gobject, id_)) + +# Toolbar/Menu actions callback + + def __close_project_cb(self, unused_button): + """Closes the current project.""" + self.app.project_manager.closeRunningProject() + + def _saveProjectCb(self, action, unused_param): + if not self.app.project_manager.current_project.uri or self.app.project_manager.disable_save: + self.saveProjectAs() + else: + self.app.project_manager.saveProject() + + def _saveProjectAsCb(self, unused_action, unused_param): + self.saveProjectAs() + + def saveProject(self): + self._saveProjectCb(None, None) + + def saveProjectAsDialog(self): + self._saveProjectAsCb(None, None) + + def _revertToSavedProjectCb(self, unused_action): + return self.app.project_manager.revertToSavedProject() + + def _exportProjectAsTarCb(self, unused_action): + uri = self._showExportDialog(self.app.project_manager.current_project) + result = None + if uri: + result = self.app.project_manager.exportProject( + self.app.project_manager.current_project, uri) + + if not result: + self.log("Project couldn't be exported") + return result + + def _projectSettingsCb(self, unused_action): + self.showProjectSettingsDialog() + + def showProjectSettingsDialog(self): + project = self.app.project_manager.current_project + dialog = ProjectSettingsDialog(self.app.gui, project, self.app) + dialog.window.run() + self.updateTitle() + + def _prefsCb(self, unused_action): + PreferencesDialog(self.app).run() + +# Project management callbacks + + def _projectManagerNewProjectLoadedCb(self, project_manager, project): + """Starts connecting the UI to the specified project. + + Args: + project_manager (ProjectManager): The project manager. + project (Project): The project which has been loaded. + """ + self.log("A new project has been loaded") + + self._connectToProject(project) + project.pipeline.activatePositionListener() + self._setProject(project) + + self.updateTitle() + + if project_manager.disable_save is True: + # Special case: we enforce "Save as", but the normal "Save" button + # redirects to it if needed, so we still want it to be enabled: + self.save_action.set_enabled(True) + + if project.ges_timeline.props.duration != 0: + self.render_button.set_sensitive(True) + + def _projectManagerSaveProjectFailedCb(self, unused_project_manager, uri, exception=None): + project_filename = unquote(uri.split("/")[-1]) + dialog = Gtk.MessageDialog(transient_for=self.app.gui, + modal=True, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, + text=_('Unable to save project "%s"') % project_filename) + if exception: + dialog.set_property("secondary-use-markup", True) + dialog.set_property("secondary-text", unquote(str(exception))) + dialog.set_transient_for(self.app.gui) + dialog.run() + dialog.destroy() + self.error("failed to save project") + + def _projectManagerProjectSavedCb(self, unused_project_manager, project, uri): + # FIXME GES: Reimplement Undo/Redo + # self.app.action_log.checkpoint() + self.updateTitle() + + self.save_action.set_enabled(False) + if project.uri is None: + project.uri = uri + + def _projectManagerClosingProjectCb(self, project_manager, project): + """Investigates whether it's possible to close the specified project. + + Args: + project_manager (ProjectManager): The project manager. + project (Project): The project which has been closed. + + Returns: + bool: True when it's OK to close it, False when the user chooses + to cancel the closing operation. + """ + if not project.hasUnsavedModifications(): + return True + + if project.uri and not project_manager.disable_save: + save = _("Save") + else: + save = _("Save as...") + + dialog = Gtk.Dialog(title="", transient_for=self.app.gui, modal=True) + dialog.add_buttons(_("Close without saving"), Gtk.ResponseType.REJECT, + _("Cancel"), Gtk.ResponseType.CANCEL, + save, Gtk.ResponseType.YES) + # Even though we set the title to an empty string when creating dialog, + # seems we really have to do it once more so it doesn't show + # "pitivi"... + dialog.set_resizable(False) + dialog.set_default_response(Gtk.ResponseType.CANCEL) + dialog.get_accessible().set_name("unsaved changes dialog") + + primary = Gtk.Label() + primary.set_line_wrap(True) + primary.set_use_markup(True) + primary.set_alignment(0, 0.5) + + message = _("Save changes to the current project before closing?") + primary.set_markup("" + message + "") + + secondary = Gtk.Label() + secondary.set_line_wrap(True) + secondary.set_use_markup(True) + secondary.set_alignment(0, 0.5) + + if project.uri: + path = unquote(project.uri).split("file://")[1] + last_saved = max( + os.path.getmtime(path), project_manager.time_loaded) + time_delta = time() - last_saved + message = _("If you don't save, " + "the changes from the last %s will be lost.") % \ + beautify_time_delta(time_delta) + else: + message = _("If you don't save, your changes will be lost.") + secondary.props.label = message + + # put the text in a vbox + vbox = Gtk.Box(homogeneous=False, spacing=SPACING * 2) + vbox.set_orientation(Gtk.Orientation.VERTICAL) + vbox.pack_start(primary, True, True, 0) + vbox.pack_start(secondary, True, True, 0) + + # make the [[image] text] hbox + image = Gtk.Image.new_from_icon_name( + "dialog-question", Gtk.IconSize.DIALOG) + hbox = Gtk.Box(homogeneous=False, spacing=SPACING * 2) + hbox.set_orientation(Gtk.Orientation.HORIZONTAL) + hbox.pack_start(image, False, False, 0) + hbox.pack_start(vbox, True, True, 0) + hbox.set_border_width(SPACING) + + # stuff the hbox in the dialog + content_area = dialog.get_content_area() + content_area.pack_start(hbox, True, True, 0) + content_area.set_spacing(SPACING * 2) + hbox.show_all() + + response = dialog.run() + dialog.destroy() + if response == Gtk.ResponseType.YES: + if project.uri is not None and project_manager.disable_save is False: + res = self.app.project_manager.saveProject() + else: + res = self.saveProjectAs() + elif response == Gtk.ResponseType.REJECT: + res = True + else: + res = False + + return res + + def _projectManagerProjectClosedCb(self, unused_project_manager, project): + """Starts disconnecting the UI from the specified project. + + This happens when the user closes the app or asks to load another + project, immediately after the user confirmed that unsaved changes, + if any, can be discarded but before the filechooser to pick the next + project to load appears. + + Args: + project (Project): The project which has been closed. + """ + + # We must disconnect from the project pipeline before it is released: + if project.pipeline is not None: + project.pipeline.deactivatePositionListener() + + self.info("Project closed") + self.updateTitle() + if project.loaded: + self._disconnectFromProject(project) + self.timeline_ui.setProject(None) + self.render_button.set_sensitive(False) + return False + + def _projectManagerRevertingToSavedCb(self, unused_project_manager, unused_project): + if self.app.project_manager.current_project.hasUnsavedModifications(): + dialog = Gtk.MessageDialog(transient_for=self.app.gui, + modal=True, + message_type=Gtk.MessageType.WARNING, + buttons=Gtk.ButtonsType.NONE, + text=_("Revert to saved project version?")) + dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.NO, + Gtk.STOCK_REVERT_TO_SAVED, Gtk.ResponseType.YES) + dialog.set_resizable(False) + dialog.set_property("secondary-text", + _("This will reload the current project. All unsaved changes will be lost.")) + dialog.set_default_response(Gtk.ResponseType.NO) + dialog.set_transient_for(self.app.gui) + response = dialog.run() + dialog.destroy() + if response != Gtk.ResponseType.YES: + return False + return True + + def _projectManagerMissingUriCb(self, project_manager, project, unused_error, asset): + if project.at_least_one_asset_missing: + # One asset is already missing so no point in spamming the user + # with more file-missing dialogs, as we need all of them. + return None + + if self.app.proxy_manager.is_proxy_asset(asset): + uri = self.app.proxy_manager.getTargetUri(asset) + else: + uri = asset.get_id() + dialog = Gtk.Dialog(title=_("Locate missing file..."), + transient_for=self.app.gui, + modal=True) + + dialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, + _("Open"), Gtk.ResponseType.OK) + dialog.set_border_width(SPACING * 2) + dialog.get_content_area().set_spacing(SPACING) + dialog.set_transient_for(self.app.gui) + dialog.set_default_response(Gtk.ResponseType.OK) + + # This box will contain widgets with details about the missing file. + vbox = Gtk.Box() + vbox.set_orientation(Gtk.Orientation.VERTICAL) + + label_start = Gtk.Label() + label_start.set_markup(_("The following file could not be found:")) + label_start.set_xalign(0) + vbox.pack_start(label_start, False, False, 0) + + hbox = Gtk.Box() + hbox.set_orientation(Gtk.Orientation.HORIZONTAL) + hbox.set_margin_top(PADDING) + hbox.set_spacing(PADDING * 2) + + label_asset_info = Gtk.Label() + label_asset_info.set_markup(beautify_missing_asset(asset)) + label_asset_info.set_xalign(0) + label_asset_info.set_yalign(0) + hbox.pack_start(label_asset_info, False, False, 0) + + small_thumb, large_thumb = AssetThumbnail.get_thumbnails_from_xdg_cache(uri) + if large_thumb: + self.debug("A thumbnail file was found for %s", uri) + thumbnail = Gtk.Image.new_from_pixbuf(large_thumb) + hbox.pack_end(thumbnail, False, False, 0) + + vbox.pack_start(hbox, False, False, 0) + + label_end = Gtk.Label() + label_end.set_markup(_("Please specify its new location:")) + label_end.set_xalign(0) + label_end.set_margin_top(PADDING) + vbox.pack_start(label_end, False, False, 0) + + dialog.get_content_area().pack_start(vbox, False, False, 0) + vbox.show_all() + + chooser = Gtk.FileChooserWidget(action=Gtk.FileChooserAction.OPEN) + chooser.set_select_multiple(False) + previewer = PreviewWidget(self.settings, discover_sync=True) + chooser.set_preview_widget(previewer) + chooser.set_use_preview_label(False) + chooser.connect('update-preview', previewer.update_preview_cb) + chooser.set_current_folder(self.settings.lastProjectFolder) + # Use a Gtk FileFilter to only show files with the same extension + # Note that splitext gives us the extension with the ".", no need to + # add it inside the filter string. + unused_filename, extension = os.path.splitext(uri) + filter_ = Gtk.FileFilter() + # Translators: this is a format filter in a filechooser. Ex: "AVI + # files" + filter_.set_name(_("%s files") % extension) + filter_.add_pattern("*%s" % extension.lower()) + filter_.add_pattern("*%s" % extension.upper()) + default = Gtk.FileFilter() + default.set_name(_("All files")) + default.add_pattern("*") + chooser.add_filter(filter_) + chooser.add_filter(default) + dialog.get_content_area().pack_start(chooser, True, True, 0) + chooser.show() + + # If the window is too big, the window manager will resize it so that + # it fits on the screen. + dialog.set_default_size(1024, 1000) + response = dialog.run() + + new_uri = None + if response == Gtk.ResponseType.OK: + self.log("User chose a new URI for the missing file") + new_uri = chooser.get_uri() + else: + dialog.hide() + + if not self.app.proxy_manager.checkProxyLoadingSucceeded(asset): + # Reset the project manager and disconnect all the signals. + project_manager.closeRunningProject() + # Signal the project loading failure. + # You have to do this *after* successfully creating a blank project, + # or the startupwizard will still be connected to that signal too. + reason = _('No replacement file was provided for "%s".\n\n' + 'Pitivi does not currently support partial projects.') % \ + info_name(asset) + project_manager.emit("new-project-failed", project.uri, reason) + + dialog.destroy() + return new_uri + + def _connectToProject(self, project): + # FIXME GES we should re-enable this when possible + # medialibrary.connect("missing-plugins", self._sourceListMissingPluginsCb) + project.connect("project-changed", self._projectChangedCb) + project.connect("rendering-settings-changed", + self._rendering_settings_changed_cb) + project.ges_timeline.connect("notify::duration", + self._timelineDurationChangedCb) + + def _sourceListMissingPluginsCb( + self, unused_project, unused_uri, unused_factory, + details, unused_descriptions, missingPluginsCallback): + res = self._installPlugins(details, missingPluginsCallback) + return res + + def _installPlugins(self, details, missingPluginsCallback): + context = GstPbutils.InstallPluginsContext() + if self.app.system.has_x11(): + context.set_xid(self.window.xid) + + res = GstPbutils.install_plugins_async(details, context, + missingPluginsCallback) + return res + + def _setProject(self, project): + """Disconnects and then reconnects callbacks to the specified project. + + Args: + project (Project): The new current project. + """ + if not project: + self.warning("Current project instance does not exist") + return False + + self.viewer.setPipeline(project.pipeline) + self._reset_viewer_aspect_ratio(project) + self.clipconfig.project = project + + # When creating a blank project there's no project URI yet. + if project.uri: + folder_path = os.path.dirname(path_from_uri(project.uri)) + self.settings.lastProjectFolder = folder_path + + def _disconnectFromProject(self, project): + project.disconnect_by_func(self._projectChangedCb) + project.disconnect_by_func(self._rendering_settings_changed_cb) + project.ges_timeline.disconnect_by_func(self._timelineDurationChangedCb) + + def _rendering_settings_changed_cb(self, project, unused_item): + """Handles Project metadata changes.""" + self._reset_viewer_aspect_ratio(project) + + def _reset_viewer_aspect_ratio(self, project): + """Resets the viewer aspect ratio.""" + self.viewer.setDisplayAspectRatio(project.getDAR()) + self.viewer.timecode_entry.setFramerate(project.videorate) + + def _timelineDurationChangedCb(self, timeline, unused_duration): + """Updates the render button. + + This covers the case when a clip is inserted into a blank timeline. + This callback is not triggered by loading a project. + """ + duration = timeline.get_duration() + self.debug("Timeline duration changed to %s", duration) + self.render_button.set_sensitive(duration > 0) + + def _showExportDialog(self, project): + self.log("Export requested") + chooser = Gtk.FileChooserDialog(title=_("Export To..."), + transient_for=self.app.gui, + action=Gtk.FileChooserAction.SAVE) + chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, + _("Save"), Gtk.ResponseType.OK) + chooser.set_default_response(Gtk.ResponseType.OK) + + chooser.set_select_multiple(False) + chooser.props.do_overwrite_confirmation = True + + asset = GES.Formatter.get_default() + asset_extension = asset.get_meta(GES.META_FORMATTER_EXTENSION) + + if not project.name: + chooser.set_current_name( + _("Untitled") + "." + asset_extension + "_tar") + else: + chooser.set_current_name( + project.name + "." + asset_extension + "_tar") + + filt = Gtk.FileFilter() + filt.set_name(_("Tar archive")) + filt.add_pattern("*.%s_tar" % asset_extension) + chooser.add_filter(filt) + default = Gtk.FileFilter() + default.set_name(_("Detect automatically")) + default.add_pattern("*") + chooser.add_filter(default) + + response = chooser.run() + if response == Gtk.ResponseType.OK: + self.log("User chose a URI to export project to") + # need to do this to work around bug in Gst.uri_construct + # which escapes all /'s in path! + uri = "file://" + chooser.get_filename() + self.log("uri: %s", uri) + ret = uri + else: + self.log("User didn't choose a URI to export project to") + ret = None + + chooser.destroy() + return ret + + def saveProjectAs(self): + uri = self._showSaveAsDialog() + if uri is None: + return False + return self.app.project_manager.saveProject(uri) + + def _showSaveAsDialog(self): + self.log("Save URI requested") + chooser = Gtk.FileChooserDialog(title=_("Save As..."), + transient_for=self.app.gui, + action=Gtk.FileChooserAction.SAVE) + chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, + _("Save"), Gtk.ResponseType.OK) + chooser.set_default_response(Gtk.ResponseType.OK) + asset = GES.Formatter.get_default() + filt = Gtk.FileFilter() + filt.set_name(asset.get_meta(GES.META_DESCRIPTION)) + filt.add_pattern("*.%s" % asset.get_meta(GES.META_FORMATTER_EXTENSION)) + chooser.add_filter(filt) + + chooser.set_select_multiple(False) + chooser.set_current_name(_("Untitled") + "." + + asset.get_meta(GES.META_FORMATTER_EXTENSION)) + chooser.set_current_folder(self.settings.lastProjectFolder) + chooser.props.do_overwrite_confirmation = True + + default = Gtk.FileFilter() + default.set_name(_("Detect automatically")) + default.add_pattern("*") + chooser.add_filter(default) + + response = chooser.run() + if response == Gtk.ResponseType.OK: + self.log("User chose a URI to save project to") + # need to do this to work around bug in Gst.uri_construct + # which escapes all /'s in path! + uri = "file://" + chooser.get_filename() + file_filter = chooser.get_filter().get_name() + self.log("uri:%s , filter:%s", uri, file_filter) + self.settings.lastProjectFolder = chooser.get_current_folder() + ret = uri + else: + self.log("User didn't choose a URI to save project to") + ret = None + + chooser.destroy() + return ret + + def _screenshotCb(self, unused_action): + """Exports a snapshot of the current frame as an image file.""" + foo = self._showSaveScreenshotDialog() + if foo: + path, mime = foo[0], foo[1] + self.app.project_manager.current_project.pipeline.save_thumbnail( + -1, -1, mime, path) + + def _showSaveScreenshotDialog(self): + """Asks the user where to save the current frame. + + Returns: + List[str]: The full path and the mimetype if successful, None otherwise. + """ + chooser = Gtk.FileChooserDialog(title=_("Save As..."), + transient_for=self.app.gui, action=Gtk.FileChooserAction.SAVE) + chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, + _("Save"), Gtk.ResponseType.OK) + chooser.set_default_response(Gtk.ResponseType.OK) + chooser.set_select_multiple(False) + chooser.set_current_name(_("Untitled")) + chooser.props.do_overwrite_confirmation = True + formats = {_("PNG image"): ["image/png", ("png",)], + _("JPEG image"): ["image/jpeg", ("jpg", "jpeg")]} + for format in formats: + filt = Gtk.FileFilter() + filt.set_name(format) + filt.add_mime_type(formats.get(format)[0]) + chooser.add_filter(filt) + response = chooser.run() + if response == Gtk.ResponseType.OK: + chosen_format = formats.get(chooser.get_filter().get_name()) + chosen_ext = chosen_format[1][0] + chosen_mime = chosen_format[0] + uri = os.path.join( + chooser.get_current_folder(), chooser.get_filename()) + ret = ["%s.%s" % (uri, chosen_ext), chosen_mime] + else: + ret = None + chooser.destroy() + return ret + + def updateTitle(self): + project = self.app.project_manager.current_project + if project: + if project.name: + name = project.name + else: + name = _("Untitled") + unsaved_mark = "" + if project.hasUnsavedModifications(): + unsaved_mark = "*" + title = "%s%s — %s" % (unsaved_mark, name, APPNAME) + else: + title = APPNAME + event_box = Gtk.EventBox() + label = Gtk.Label() + clear_styles(label) + label.set_text(title) + event_box.add(label) + event_box.show_all() + event_box.connect("button-press-event", self.__titleClickCb, project) + self.headerbar.set_custom_title(event_box) + self.app.gui.set_title(title) + + def __titleClickCb(self, unused_widget, unused_event, project): + entry = Gtk.Entry() + entry.set_width_chars(100) + entry.set_margin_left(SPACING) + entry.set_margin_right(SPACING) + entry.show() + entry.set_text(project.name) + self.headerbar.set_custom_title(entry) + if project.hasDefaultName(): + entry.grab_focus() + else: + entry.grab_focus_without_selecting() + entry.connect("focus-out-event", self.__titleChangedCb, project) + entry.connect("key_release_event", self.__titleTypeCb, project) + + def __titleChangedCb(self, widget, event, project): + if not event.window: + # Workaround https://bugzilla.gnome.org/show_bug.cgi?id=757036 + return + name = widget.get_text() + if project.name == name: + self.updateTitle() + else: + project.name = name + + def __titleTypeCb(self, widget, event, project): + if event.keyval == Gdk.KEY_Return: + self.focusTimeline() + return True + elif event.keyval == Gdk.KEY_Escape: + widget.set_text(project.name) + self.focusTimeline() + return True + return False + + +class PreviewAssetWindow(Gtk.Window): + """Window for previewing a video or audio asset. + + Args: + asset (GES.UriClipAsset): The asset to be previewed. + app (Pitivi): The app. + """ + + def __init__(self, asset, app): + Gtk.Window.__init__(self) + self._asset = asset + self.app = app + + self.set_title(_("Preview")) + self.set_type_hint(Gdk.WindowTypeHint.UTILITY) + self.set_transient_for(app.gui) + + self._previewer = PreviewWidget(app.settings, minimal=True) + self.add(self._previewer) + self._previewer.preview_uri(self._asset.get_id()) + self._previewer.show() + + self.connect("focus-out-event", self._leavePreviewCb) + self.connect("key-press-event", self._keyPressCb) + + def preview(self): + """Shows the window and starts the playback.""" + width, height = self._calculatePreviewWindowSize() + self.resize(width, height) + # Setting the position of the window only works if it's currently hidden + # otherwise, after the resize the position will not be readjusted + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.show() + + self._previewer.play() + # Hack so that we really really force the "utility" window to be + # focused + self.present() + + def _calculatePreviewWindowSize(self): + info = self._asset.get_info() + video_streams = info.get_video_streams() + if not video_streams: + # There is no video/image stream. This is an audio file. + # Resize to the minimum and let the window manager deal with it. + return 1, 1 + # For videos and images, automatically resize the window + # Try to keep it 1:1 if it can fit within 85% of the parent window + video = video_streams[0] + img_width = video.get_square_width() + img_height = video.get_height() + mainwindow_width, mainwindow_height = self.app.gui.get_size() + max_width = 0.85 * mainwindow_width + max_height = 0.85 * mainwindow_height + + controls_height = self._previewer.bbox.get_preferred_size()[0].height + if img_width < max_width and (img_height + controls_height) < max_height: + # The video is small enough, keep it 1:1 + return img_width, img_height + controls_height + else: + # The video is too big, size it down + # TODO: be smarter, figure out which (width, height) is bigger + new_height = max_width * img_height / img_width + return int(max_width), int(new_height + controls_height) + + def _leavePreviewCb(self, window, unused): + self.destroy() + return True + + def _keyPressCb(self, unused_widget, event): + if event.keyval in (Gdk.KEY_Escape, Gdk.KEY_Q, Gdk.KEY_q): + self.destroy() + elif event.keyval == Gdk.KEY_space: + self._previewer.togglePlayback() + return True diff --git a/pitivi/effects.py b/pitivi/effects.py index 54b59d2a0..face6bfdd 100644 --- a/pitivi/effects.py +++ b/pitivi/effects.py @@ -538,7 +538,7 @@ class EffectListWidget(Gtk.Box, Loggable): effect_info = self.app.effects.getInfo(effect) if not effect_info: return - timeline = self.app.gui.timeline_ui.timeline + timeline = self.app.gui.editor.timeline_ui.timeline clip = timeline.selection.getSingleClip() if not clip: return diff --git a/pitivi/greeterperspective.py b/pitivi/greeterperspective.py new file mode 100644 index 000000000..771e458e9 --- /dev/null +++ b/pitivi/greeterperspective.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Pitivi video editor +# Copyright (c) 2010 Mathieu Duponchelle +# Copyright (c) 2018 Harish Fulara +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +"""Pitivi's Welcome/Greeter perspective.""" +import os +from gettext import gettext as _ + +from gi.repository import Gdk +from gi.repository import GES +from gi.repository import Gio +from gi.repository import Gtk + +from pitivi.configure import get_ui_dir +from pitivi.dialogs.browseprojects import BrowseProjectsDialog +from pitivi.perspective import Perspective +from pitivi.utils.ui import fix_infobar +from pitivi.utils.ui import GREETER_PERSPECTIVE_CSS + +MAX_RECENT_PROJECTS = 10 + + +class ProjectInfoRow(Gtk.ListBoxRow): + """Displays a project's info. + + Attributes: + project: Project's meta-data. + """ + def __init__(self, project): + Gtk.ListBoxRow.__init__(self) + self.uri = project.get_uri() + self.add(Gtk.Label(project.get_display_name(), xalign=0)) + + +# pylint: disable=too-many-instance-attributes +class GreeterPerspective(Perspective): + """Pitivi's Welcome/Greeter perspective. + + Allows the user to create a new project or open an existing one. + + Attributes: + app (Pitivi): The app. + """ + + def __init__(self, app): + Perspective.__init__(self) + + self.app = app + self.new_project_action = None + self.open_project_action = None + + self.__recent_projects_listbox = None + self.__project_filter = self.__create_project_filter() + self.__infobar = None + + if app.getLatest(): + self.__show_newer_available_version() + else: + app.connect("version-info-received", self.__app_version_info_received_cb) + + def setup_ui(self): + """Sets up the UI.""" + builder = Gtk.Builder() + builder.add_from_file(os.path.join(get_ui_dir(), "greeter.ui")) + + self.toplevel_widget = builder.get_object("scrolled_window") + + self.__recent_projects_listbox = builder.get_object("recent_projects_listbox") + self.__recent_projects_listbox.set_selection_mode(Gtk.SelectionMode.NONE) + self.__recent_projects_listbox.connect( + "row_activated", self.__projects_row_activated_cb) + + self.__infobar = builder.get_object("infobar") + fix_infobar(self.__infobar) + self.__infobar.hide() + self.__infobar.connect("response", self.__infobar_response_cb) + + self.__setup_css() + self.headerbar = self.__create_headerbar() + self.__set_keyboard_shortcuts() + + def refresh(self): + """Refreshes the perspective.""" + self.toplevel_widget.grab_focus() + self.__show_recent_projects() + + def __setup_css(self): + css_provider = Gtk.CssProvider() + css_provider.load_from_data(GREETER_PERSPECTIVE_CSS.encode('UTF-8')) + screen = Gdk.Screen.get_default() + style_context = self.app.gui.get_style_context() + style_context.add_provider_for_screen(screen, css_provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + def __create_headerbar(self): + headerbar = Gtk.HeaderBar() + headerbar.set_show_close_button(True) + headerbar.set_title(_("Select a Project")) + + new_project_button = Gtk.Button.new_with_label(_("New")) + new_project_button.set_tooltip_text(_("Create a new project")) + new_project_button.set_action_name("greeter.new-project") + + open_project_button = Gtk.Button.new_with_label(_("Open…")) + open_project_button.set_tooltip_text(_("Open an existing project")) + open_project_button.set_action_name("greeter.open-project") + + self.menu_button = self.__create_menu() + + headerbar.pack_start(new_project_button) + headerbar.pack_start(open_project_button) + headerbar.pack_end(self.menu_button) + headerbar.show_all() + + return headerbar + + def __set_keyboard_shortcuts(self): + group = Gio.SimpleActionGroup() + self.toplevel_widget.insert_action_group("greeter", group) + self.headerbar.insert_action_group("greeter", group) + + self.new_project_action = Gio.SimpleAction.new("new-project", None) + self.new_project_action.connect("activate", self.__new_project_cb) + group.add_action(self.new_project_action) + self.app.shortcuts.add("greeter.new-project", ["n"], + _("Create a new project"), group="win") + + self.open_project_action = Gio.SimpleAction.new("open-project", None) + self.open_project_action.connect("activate", self.__open_project_cb) + group.add_action(self.open_project_action) + self.app.shortcuts.add("greeter.open-project", ["o"], + _("Open a project"), group="win") + + def __show_recent_projects(self): + """Displays recent projects.""" + # Clear the currently displayed list. + for child in self.__recent_projects_listbox.get_children(): + self.__recent_projects_listbox.remove(child) + + recent_items = [item for item in self.app.recent_manager.get_items() + if item.get_display_name().endswith(self.__project_filter)] + + for item in recent_items[:MAX_RECENT_PROJECTS]: + self.__recent_projects_listbox.add(ProjectInfoRow(item)) + + self.__recent_projects_listbox.show_all() + + @staticmethod + def __create_project_filter(): + filter_ = [] + for asset in GES.list_assets(GES.Formatter): + filter_.append(asset.get_meta(GES.META_FORMATTER_EXTENSION)) + return tuple(filter_) + + @staticmethod + def __create_menu(): + builder = Gtk.Builder() + builder.add_from_file(os.path.join(get_ui_dir(), "mainmenubutton.ui")) + menu_button = builder.get_object("menubutton") + # Menu options we want to display. + visible_options = ["menu_shortcuts", "menu_help", "menu_about"] + for widget in builder.get_object("menu").get_children(): + if Gtk.Buildable.get_name(widget) not in visible_options: + widget.hide() + else: + visible_options.remove(Gtk.Buildable.get_name(widget)) + assert not visible_options + return menu_button + + def __new_project_cb(self, unused_action, unused_param): + self.app.project_manager.newBlankProject() + + def __open_project_cb(self, unused_action, unused_param): + dialog = BrowseProjectsDialog(self.app) + response = dialog.run() + uri = dialog.get_uri() + dialog.destroy() + if response == Gtk.ResponseType.OK: + self.app.project_manager.loadProject(uri) + + def __app_version_info_received_cb(self, app, unused_version_information): + """Handles new version info.""" + if app.isLatest(): + # current version, don't show message + return + self.__show_newer_available_version() + + def __show_newer_available_version(self): + latest_version = self.app.getLatest() + + if self.app.settings.lastCurrentVersion != latest_version: + # new latest version, reset counter + self.app.settings.lastCurrentVersion = latest_version + self.app.settings.displayCounter = 0 + + if self.app.settings.displayCounter >= 5: + # current version info already showed 5 times, don't show again + return + + # increment counter, create infobar and show info + self.app.settings.displayCounter += 1 + text = _("Pitivi %s is available.") % latest_version + label = Gtk.Label(label=text) + self.__infobar.get_content_area().add(label) + self.__infobar.set_message_type(Gtk.MessageType.INFO) + self.__infobar.show_all() + + def __infobar_response_cb(self, unused_infobar, response_id): + if response_id == Gtk.ResponseType.CLOSE: + self.__infobar.hide() + + def __projects_row_activated_cb(self, unused_listbox, row): + self.app.project_manager.loadProject(row.uri) diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py index 79d76f269..8397be71b 100644 --- a/pitivi/mainwindow.py +++ b/pitivi/mainwindow.py @@ -16,63 +16,23 @@ # License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301, USA. +"""Pitivi's main window.""" import os from gettext import gettext as _ -from time import time from urllib.parse import unquote -from gi.repository import Gdk -from gi.repository import GES from gi.repository import Gio -from gi.repository import Gst -from gi.repository import GstPbutils from gi.repository import Gtk -from pitivi.clipproperties import ClipProperties -from pitivi.configure import APPNAME -from pitivi.configure import APPURL from pitivi.configure import get_pixmap_dir -from pitivi.configure import get_ui_dir -from pitivi.configure import GITVERSION -from pitivi.configure import in_devel -from pitivi.configure import VERSION -from pitivi.dialogs.prefs import PreferencesDialog -from pitivi.effects import EffectListWidget -from pitivi.mediafilespreviewer import PreviewWidget -from pitivi.medialibrary import AssetThumbnail -from pitivi.medialibrary import MediaLibraryWidget -from pitivi.project import ProjectSettingsDialog +from pitivi.dialogs.about import AboutDialog +from pitivi.editorperspective import EditorPerspective +from pitivi.greeterperspective import GreeterPerspective from pitivi.settings import GlobalSettings -from pitivi.tabsmanager import BaseTabs -from pitivi.timeline.timeline import TimelineContainer -from pitivi.titleeditor import TitleEditor -from pitivi.transitions import TransitionsListWidget from pitivi.utils.loggable import Loggable -from pitivi.utils.misc import path_from_uri from pitivi.utils.misc import show_user_manual -from pitivi.utils.ui import beautify_missing_asset -from pitivi.utils.ui import beautify_time_delta -from pitivi.utils.ui import clear_styles -from pitivi.utils.ui import info_name -from pitivi.utils.ui import PADDING -from pitivi.utils.ui import SPACING -from pitivi.utils.ui import TIMELINE_CSS -from pitivi.viewer.viewer import ViewerContainer -GlobalSettings.addConfigSection("main-window") -GlobalSettings.addConfigOption('mainWindowHPanePosition', - section="main-window", - key="hpane-position", - type_=int) -GlobalSettings.addConfigOption('mainWindowMainHPanePosition', - section="main-window", - key="main-hpane-position", - type_=int) -GlobalSettings.addConfigOption('mainWindowVPanePosition', - section="main-window", - key="vpane-position", - type_=int) GlobalSettings.addConfigOption('mainWindowX', section="main-window", key="X", default=0, type_=int) @@ -85,17 +45,14 @@ GlobalSettings.addConfigOption('mainWindowWidth', GlobalSettings.addConfigOption('mainWindowHeight', section="main-window", key="height", default=-1, type_=int) -GlobalSettings.addConfigOption('lastProjectFolder', - section="main-window", - key="last-folder", - environment="PITIVI_PROJECT_FOLDER", - default=os.path.expanduser("~")) + GlobalSettings.addConfigSection('export') GlobalSettings.addConfigOption('lastExportFolder', section='export', key="last-export-folder", environment="PITIVI_EXPORT_FOLDER", default=os.path.expanduser("~")) + GlobalSettings.addConfigSection("version") GlobalSettings.addConfigOption('displayCounter', section='version', @@ -110,6 +67,9 @@ GlobalSettings.addConfigOption('lastCurrentVersion', class MainWindow(Gtk.ApplicationWindow, Loggable): """Pitivi's main window. + It manages the UI and handles the switch between different perspectives, + such as the default GreeterPerspective, and the EditorPerspective. + Attributes: app (Pitivi): The app. """ @@ -125,213 +85,51 @@ class MainWindow(Gtk.ApplicationWindow, Loggable): os.environ["PULSE_PROP_media.role"] = "production" os.environ["PULSE_PROP_application.icon_name"] = "pitivi" - Gtk.ApplicationWindow.__init__(self) - Loggable.__init__(self) - self.app = app - self.log("Creating MainWindow") - self.settings = app.settings - Gtk.IconTheme.get_default().append_search_path(get_pixmap_dir()) - self.connect("destroy", self._destroyedCb) - - self.setupCss() - self.builder_handler_ids = [] - self.builder = Gtk.Builder() - - self._createUi() - self.recent_manager = Gtk.RecentManager() - - pm = self.app.project_manager - pm.connect("new-project-loading", - self._projectManagerNewProjectLoadingCb) - pm.connect("new-project-loaded", - self._projectManagerNewProjectLoadedCb) - pm.connect("new-project-failed", - self._projectManagerNewProjectFailedCb) - pm.connect("save-project-failed", - self._projectManagerSaveProjectFailedCb) - pm.connect("project-saved", self._projectManagerProjectSavedCb) - pm.connect("closing-project", self._projectManagerClosingProjectCb) - pm.connect("reverting-to-saved", - self._projectManagerRevertingToSavedCb) - pm.connect("project-closed", self._projectManagerProjectClosedCb) - pm.connect("missing-uri", self._projectManagerMissingUriCb) - - def setupCss(self): - css_provider = Gtk.CssProvider() - css_provider.load_from_data(TIMELINE_CSS.encode('UTF-8')) - screen = Gdk.Screen.get_default() - style_context = self.get_style_context() - style_context.add_provider_for_screen(screen, css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - - def showRenderDialog(self): - """Shows the RenderDialog for the current project.""" - from pitivi.render import RenderDialog - - project = self.app.project_manager.current_project - dialog = RenderDialog(self.app, project) - dialog.window.show() - - def _destroyedCb(self, unused_self): - self.render_button.disconnect_by_func(self._renderCb) - pm = self.app.project_manager - pm.disconnect_by_func(self._projectManagerNewProjectLoadingCb) - pm.disconnect_by_func(self._projectManagerNewProjectLoadedCb) - pm.disconnect_by_func(self._projectManagerNewProjectFailedCb) - pm.disconnect_by_func(self._projectManagerSaveProjectFailedCb) - pm.disconnect_by_func(self._projectManagerProjectSavedCb) - pm.disconnect_by_func(self._projectManagerClosingProjectCb) - pm.disconnect_by_func(self._projectManagerRevertingToSavedCb) - pm.disconnect_by_func(self._projectManagerProjectClosedCb) - pm.disconnect_by_func(self._projectManagerMissingUriCb) - self.save_action.disconnect_by_func(self._saveProjectCb) - self.new_project_action.disconnect_by_func(self._newProjectMenuCb) - self.open_project_action.disconnect_by_func(self._openProjectCb) - self.save_as_action.disconnect_by_func(self._saveProjectAsCb) - self.help_action.disconnect_by_func(self._userManualCb) - self.menu_button_action.disconnect_by_func(self._menuCb) - self.disconnect_by_func(self._destroyedCb) - self.disconnect_by_func(self._configureCb) - for gobject, id_ in self.builder_handler_ids: - gobject.disconnect(id_) - self.builder_handler_ids = None - self.vpaned.remove(self.timeline_ui) - self.timeline_ui.destroy() - - def _renderCb(self, unused_button): - self.showRenderDialog() + Gtk.ApplicationWindow.__init__(self) + Loggable.__init__(self) - def _createUi(self): - """Creates the graphical interface. + self.log("Creating main window") - The rough hierarchy is: - vpaned: - - mainhpaned(secondhpaned(main_tabs, context_tabs), viewer) - - timeline_ui + self.app = app + self.greeter = GreeterPerspective(app) + self.editor = EditorPerspective(app) + self.__perspective = None + self.help_action = None + self.about_action = None + self.main_menu_action = None + + app.project_manager.connect("new-project-loading", + self.__new_project_loading_cb) + app.project_manager.connect("new-project-failed", + self.__new_project_failed_cb) + app.project_manager.connect("project-closed", self.__project_closed_cb) + + def setup_ui(self): + """Sets up the various perspectives's UI.""" + self.log("Setting up the perspectives.") - The full hierarchy can be admired by starting the GTK+ Inspector - with Ctrl+Shift+I. - """ self.set_icon_name("pitivi") + self.__check_screen_constraints() + self.__set_keyboard_shortcuts() - # Main "toolbar" (using client-side window decorations with HeaderBar) - self._headerbar = Gtk.HeaderBar() - self._create_headerbar_buttons() - self._headerbar.set_show_close_button(True) - self._headerbar.show_all() - self.set_titlebar(self._headerbar) - - # Set up our main containers, in the order documented above - self.vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) # Separates the tabs+viewer from the timeline - self.mainhpaned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) # Separates the tabs from the viewer - self.secondhpaned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) # Separates the two sets of tabs - self.vpaned.pack1(self.mainhpaned, resize=False, shrink=False) - self.mainhpaned.pack1(self.secondhpaned, resize=True, shrink=False) - self.add(self.vpaned) - self.vpaned.show() - self.secondhpaned.show() - self.mainhpaned.show() - - # First set of tabs - self.main_tabs = BaseTabs(self.app) - self.medialibrary = MediaLibraryWidget(self.app) - self.effectlist = EffectListWidget(self.app) - self.main_tabs.append_page("Media Library", - self.medialibrary, Gtk.Label(label=_("Media Library"))) - self.main_tabs.append_page("Effect Library", - self.effectlist, Gtk.Label(label=_("Effect Library"))) - self.medialibrary.connect('play', self._mediaLibraryPlayCb) - self.medialibrary.show() - self.effectlist.show() - - # Second set of tabs - self.context_tabs = BaseTabs(self.app) - self.clipconfig = ClipProperties(self.app) - self.trans_list = TransitionsListWidget(self.app) - self.title_editor = TitleEditor(self.app) - self.context_tabs.append_page("Clip", - self.clipconfig, Gtk.Label(label=_("Clip"))) - self.context_tabs.append_page("Transition", - self.trans_list, Gtk.Label(label=_("Transition"))) - self.context_tabs.append_page("Title", - self.title_editor.widget, Gtk.Label(label=_("Title"))) - # Show by default the Title tab, as the Clip and Transition tabs - # are useful only when a clip or transition is selected, but - # the Title tab allows adding titles. - self.context_tabs.set_current_page(2) - - self.secondhpaned.pack1(self.main_tabs, resize=False, shrink=False) - self.secondhpaned.pack2(self.context_tabs, resize=False, shrink=False) - self.main_tabs.show() - self.context_tabs.show() + self.greeter.setup_ui() + self.editor.setup_ui() - # Viewer - self.viewer = ViewerContainer(self.app) - self.mainhpaned.pack2(self.viewer, resize=True, shrink=False) + width = self.app.settings.mainWindowWidth + height = self.app.settings.mainWindowHeight - # Now, the lower part: the timeline - self.timeline_ui = TimelineContainer(self.app) - self.vpaned.pack2(self.timeline_ui, resize=True, shrink=False) - - # Enable our shortcuts for HeaderBar buttons and menu items: - self._set_keyboard_shortcuts() - - # Identify widgets for AT-SPI, making our test suite easier to develop - # These will show up in sniff, accerciser, etc. - self.get_accessible().set_name("main window") - self._headerbar.get_accessible().set_name("headerbar") - self._menubutton.get_accessible().set_name("main menu button") - self.vpaned.get_accessible().set_name("contents") - self.mainhpaned.get_accessible().set_name("upper half") - self.secondhpaned.get_accessible().set_name("tabs") - self.main_tabs.get_accessible().set_name("primary tabs") - self.context_tabs.get_accessible().set_name("secondary tabs") - self.viewer.get_accessible().set_name("viewer") - self.timeline_ui.get_accessible().set_name("timeline area") - - # Restore settings for position and visibility. - if self.settings.mainWindowHPanePosition is None: - self._setDefaultPositions() - width = self.settings.mainWindowWidth - height = self.settings.mainWindowHeight if height == -1 and width == -1: self.maximize() else: self.set_default_size(width, height) - self.move(self.settings.mainWindowX, self.settings.mainWindowY) - self.secondhpaned.set_position(self.settings.mainWindowHPanePosition) - self.mainhpaned.set_position(self.settings.mainWindowMainHPanePosition) - self.vpaned.set_position(self.settings.mainWindowVPanePosition) - - # Connect the main window's signals at the end, to avoid messing around - # with the restoration of settings above. - self.connect("delete-event", self._deleteCb) - self.connect("configure-event", self._configureCb) + self.move(self.app.settings.mainWindowX, self.app.settings.mainWindowY) - # Focus the timeline by default! - self.focusTimeline() - self.updateTitle() + self.connect("configure-event", self.__configure_cb) + self.connect("delete-event", self.__delete_cb) - def _setDefaultPositions(self): - window_width = self.get_size()[0] - if self.settings.mainWindowHPanePosition is None: - self.settings.mainWindowHPanePosition = window_width / 3 - if self.settings.mainWindowMainHPanePosition is None: - self.settings.mainWindowMainHPanePosition = 2 * window_width / 3 - if self.settings.mainWindowVPanePosition is None: - screen_width = float(self.get_screen().get_width()) - screen_height = float(self.get_screen().get_height()) - req = self.vpaned.get_preferred_size()[0] - if screen_width / screen_height < 0.75: - # Tall screen, give some more vertical space the the tabs. - value = req.height / 3 - else: - value = req.height / 2 - self.settings.mainWindowVPanePosition = value - - def checkScreenConstraints(self): + def __check_screen_constraints(self): """Measures the approximate minimum size required by the main window. Shrinks some widgets to fit better on smaller screen resolutions. @@ -340,7 +138,7 @@ class MainWindow(Gtk.ApplicationWindow, Loggable): # is only an approximation. As of 2015, GTK still does not have # a way, even with client-side decorations, to tell us the exact # minimum required dimensions of a window. - min_size, natural_size = self.get_preferred_size() + min_size, _ = self.get_preferred_size() screen_width = self.get_screen().get_width() screen_height = self.get_screen().get_height() self.debug("Minimum UI size is %sx%s", min_size.width, min_size.height) @@ -348,549 +146,67 @@ class MainWindow(Gtk.ApplicationWindow, Loggable): if min_size.width >= 0.9 * screen_width: self.medialibrary.activateCompactMode() self.viewer.activateCompactMode() - min_size, natural_size = self.get_preferred_size() + min_size, _ = self.get_preferred_size() self.info("Minimum UI size has been reduced to %sx%s", min_size.width, min_size.height) - def switchContextTab(self, ges_clip): - """Activates the appropriate tab on the second set of tabs. - - Args: - ges_clip (GES.SourceClip): The clip which has been focused. - """ - if isinstance(ges_clip, GES.TitleClip): - page = 2 - elif isinstance(ges_clip, GES.SourceClip): - page = 0 - elif isinstance(ges_clip, GES.TransitionClip): - page = 1 - else: - self.warning("Unknown clip type: %s", ges_clip) - return - self.context_tabs.set_current_page(page) - - def focusTimeline(self): - layers_representation = self.timeline_ui.timeline.layout - # Check whether it has focus already, grab_focus always emits an event. - if not layers_representation.props.is_focus: - layers_representation.grab_focus() - - def _create_headerbar_buttons(self): - - undo_button = Gtk.Button.new_from_icon_name( - "edit-undo-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - undo_button.set_always_show_image(True) - undo_button.set_label(_("Undo")) - undo_button.set_action_name("app.undo") - undo_button.set_use_underline(True) - - redo_button = Gtk.Button.new_from_icon_name( - "edit-redo-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - redo_button.set_always_show_image(True) - redo_button.set_action_name("app.redo") - redo_button.set_use_underline(True) - - self.save_button = Gtk.Button.new_with_label(_("Save")) - self.save_button.set_focus_on_click(False) - - self.render_button = Gtk.Button.new_from_icon_name( - "system-run-symbolic", Gtk.IconSize.SMALL_TOOLBAR) - self.render_button.set_always_show_image(True) - self.render_button.set_label(_("Render")) - self.render_button.set_tooltip_text( - _("Export your project as a finished movie")) - self.render_button.set_sensitive(False) # The only one we have to set. - self.render_button.connect("clicked", self._renderCb) - - undo_redo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) - undo_redo_box.get_style_context().add_class("linked") - undo_redo_box.pack_start(undo_button, expand=False, fill=False, padding=0) - undo_redo_box.pack_start(redo_button, expand=False, fill=False, padding=0) - self._headerbar.pack_start(undo_redo_box) - - self.builder.add_from_file( - os.path.join(get_ui_dir(), "mainmenubutton.ui")) - - # FIXME : see https://bugzilla.gnome.org/show_bug.cgi?id=729263 - self.builder.connect_signals_full(self._builderConnectCb, self) - - self._menubutton = self.builder.get_object("menubutton") - - self._menubutton_items = {} - for widget in self.builder.get_object("menu").get_children(): - self._menubutton_items[Gtk.Buildable.get_name(widget)] = widget - - self._headerbar.pack_end(self._menubutton) - self._headerbar.pack_end(self.save_button) - self._headerbar.pack_end(self.render_button) - - def _set_keyboard_shortcuts(self): + def __set_keyboard_shortcuts(self): self.app.shortcuts.register_group("win", _("Project"), position=20) - self.save_action = Gio.SimpleAction.new("save", None) - self.save_action.connect("activate", self._saveProjectCb) - self.add_action(self.save_action) - self.app.shortcuts.add("win.save", ["s"], - _("Save the current project")) - self.save_button.set_action_name("win.save") - - self.new_project_action = Gio.SimpleAction.new("new-project", None) - self.new_project_action.connect("activate", self._newProjectMenuCb) - self.add_action(self.new_project_action) - self.app.shortcuts.add("win.new-project", ["n"], - _("Create a new project")) - - self.open_project_action = Gio.SimpleAction.new("open-project", None) - self.open_project_action.connect("activate", self._openProjectCb) - self.add_action(self.open_project_action) - self.app.shortcuts.add("win.open-project", ["o"], - _("Open a project")) - - self.save_as_action = Gio.SimpleAction.new("save-as", None) - self.save_as_action.connect("activate", self._saveProjectAsCb) - self.add_action(self.save_as_action) - self.app.shortcuts.add("win.save-as", ["s"], - _("Save the current project as")) self.help_action = Gio.SimpleAction.new("help", None) - self.help_action.connect("activate", self._userManualCb) + self.help_action.connect("activate", self.__user_manual_cb) self.add_action(self.help_action) self.app.shortcuts.add("win.help", ["F1"], _("Help"), group="app") - self.menu_button_action = Gio.SimpleAction.new("menu-button", None) - self.menu_button_action.connect("activate", self._menuCb) - self.add_action(self.menu_button_action) - self.app.shortcuts.add("win.menu-button", ["F10"], - _("Show the menu button content"), - group="app") + self.about_action = Gio.SimpleAction.new("about", None) + self.about_action.connect("activate", self.__about_cb) + self.add_action(self.about_action) + self.app.shortcuts.add("win.about", ["a"], + _("About"), group="app") - import_asset_action = Gio.SimpleAction.new("import-asset", None) - import_asset_action.connect("activate", self.__import_asset_cb) - self.add_action(import_asset_action) - self.app.shortcuts.add("win.import-asset", ["i"], - _("Add media files to your project")) + self.main_menu_action = Gio.SimpleAction.new("menu-button", None) + self.main_menu_action.connect("activate", self.__menu_cb) + self.add_action(self.main_menu_action) + self.app.shortcuts.add("win.menu-button", ["F10"], + _("Show the menu button content"), group="app") - def __import_asset_cb(self, unusdaction, unusedparam): - self.medialibrary.show_import_assets_dialog() + @staticmethod + def __user_manual_cb(unused_action, unused_param): + show_user_manual() - def showProjectStatus(self): - project = self.app.project_manager.current_project - dirty = project.hasUnsavedModifications() - self.save_action.set_enabled(dirty) - if project.uri: - self._menubutton_items["menu_revert_to_saved"].set_sensitive(dirty) - self.updateTitle() + def __about_cb(self, unused_action, unused_param): + about_dialog = AboutDialog(self.app) + about_dialog.show() -# UI Callbacks + def __menu_cb(self, unused_action, unused_param): + self.__perspective.menu_button.set_active( + not self.__perspective.menu_button.get_active()) - def _configureCb(self, unused_widget, unused_event): + def __configure_cb(self, unused_widget, unused_event): """Saves the main window position and size.""" # Takes window manager decorations into account. position = self.get_position() - self.settings.mainWindowX = position.root_x - self.settings.mainWindowY = position.root_y + self.app.settings.mainWindowX = position.root_x + self.app.settings.mainWindowY = position.root_y # Does not include the size of the window manager decorations. size = self.get_size() - self.settings.mainWindowWidth = size.width - self.settings.mainWindowHeight = size.height - - def _deleteCb(self, unused_widget, unused_data=None): - self._saveWindowSettings() - if not self.app.shutdown(): - return True - - return False - - def _saveWindowSettings(self): - self.settings.mainWindowHPanePosition = self.secondhpaned.get_position( - ) - self.settings.mainWindowMainHPanePosition = self.mainhpaned.get_position( - ) - self.settings.mainWindowVPanePosition = self.vpaned.get_position() - - def _mediaLibraryPlayCb(self, unused_medialibrary, asset): - """Previews the specified asset. - - If the media library item to preview is an image, show it in the user's - favorite image viewer. Else, preview the video/sound in Pitivi. - """ - # Technically, our preview widget can show images, but it's never going - # to do a better job (sizing, zooming, metadata, editing, etc.) - # than the user's favorite image viewer. - if asset.is_image(): - Gio.AppInfo.launch_default_for_uri(asset.get_id(), None) - else: - preview_window = PreviewAssetWindow(asset, self) - preview_window.preview() - - def _projectChangedCb(self, unused_project): - self.save_action.set_enabled(True) - self.updateTitle() - - def _builderConnectCb(self, builder, gobject, signal_name, handler_name, - connect_object, flags, user_data): - id_ = gobject.connect(signal_name, getattr(self, handler_name)) - self.builder_handler_ids.append((gobject, id_)) - -# Toolbar/Menu actions callback - - def _newProjectMenuCb(self, unused_action, unused_param): - self.app.project_manager.newBlankProject() - - def _openProjectCb(self, unused_action, unused_param): - self.openProject() - - def _saveProjectCb(self, action, unused_param): - if not self.app.project_manager.current_project.uri or self.app.project_manager.disable_save: - self.saveProjectAs() - else: - self.app.project_manager.saveProject() - - def _saveProjectAsCb(self, unused_action, unused_param): - self.saveProjectAs() - - def saveProject(self): - self._saveProjectCb(None, None) - - def saveProjectAsDialog(self): - self._saveProjectAsCb(None, None) - - def _revertToSavedProjectCb(self, unused_action): - return self.app.project_manager.revertToSavedProject() - - def _exportProjectAsTarCb(self, unused_action): - uri = self._showExportDialog(self.app.project_manager.current_project) - result = None - if uri: - result = self.app.project_manager.exportProject( - self.app.project_manager.current_project, uri) - - if not result: - self.log("Project couldn't be exported") - return result - - def _projectSettingsCb(self, unused_action): - self.showProjectSettingsDialog() - - def showProjectSettingsDialog(self): - project = self.app.project_manager.current_project - dialog = ProjectSettingsDialog(self, project, self.app) - dialog.window.run() - self.updateTitle() - - def _menuCb(self, unused_action, unused_param): - self._menubutton.set_active(not self._menubutton.get_active()) - - def _userManualCb(self, unused_action, unused_param): - show_user_manual() - - def _aboutResponseCb(self, dialog, unused_response): - dialog.destroy() - - def _aboutCb(self, unused_action): - abt = Gtk.AboutDialog() - abt.set_program_name(APPNAME) - abt.set_website(APPURL) - - if in_devel(): - version_str = _("Development version: %s") % GITVERSION - elif not self.app.isLatest(): - version_str = _("Version %(cur_ver)s — %(new_ver)s is available") % \ - {"cur_ver": GITVERSION, - "new_ver": self.app.getLatest()} - elif GITVERSION: - version_str = _("Version %s") % GITVERSION - else: - version_str = _("Version %s") % VERSION - abt.set_version(version_str) - - comments = ["", - "GES %s" % ".".join(map(str, GES.version())), - "GTK+ %s" % ".".join(map(str, (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION))), - "GStreamer %s" % ".".join(map(str, Gst.version()))] - abt.set_comments("\n".join(comments)) - - authors = [_("Current maintainers:"), - "Jean-François Fortin Tam ", - "Thibault Saunier ", - "Mathieu Duponchelle ", - "Alexandru Băluț ", - "", - _("Past maintainers:"), - "Edward Hervey ", - "Alessandro Decina ", - "Brandon Lewis ", - "", - # Translators: this paragraph is to be translated, the list - # of contributors is shown dynamically as a clickable link - # below it - _("Contributors:\n" + - "A handwritten list here would...\n" + - "• be too long,\n" + - "• be frequently outdated,\n" + - "• not show their relative merit.\n\n" + - "Out of respect for our contributors, we point you instead to:\n"), - # Translators: keep the %s at the end of the 1st line - _("The list of contributors on Ohloh %s\n" + - "Or you can run: git shortlog -s -n") - % "http://ohloh.net/p/pitivi/contributors", ] - abt.set_authors(authors) - # Translators: See - # https://developer.gnome.org/gtk3/stable/GtkAboutDialog.html#gtk-about-dialog-set-translator-credits - # for details on how this is used. - translators = _("translator-credits") - if translators != "translator-credits": - abt.set_translator_credits(translators) - documenters = ["Jean-François Fortin Tam ", ] - abt.set_documenters(documenters) - abt.set_license_type(Gtk.License.LGPL_2_1) - abt.set_icon_name("pitivi") - abt.set_logo_icon_name("pitivi") - abt.connect("response", self._aboutResponseCb) - abt.set_transient_for(self) - abt.show() - - def openProject(self): - # Requesting project closure at this point in time prompts users about - # unsaved changes (if any); much better than having ProjectManager - # trigger this *after* the user already chose a new project to load... - if not self.app.project_manager.closeRunningProject(): - return # The user has not made a decision, don't do anything - - chooser = Gtk.FileChooserDialog(title=_("Open File..."), - transient_for=self, - action=Gtk.FileChooserAction.OPEN) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Open"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - chooser.set_select_multiple(False) - # TODO: Remove this set_current_folder call when GTK bug 683999 is - # fixed - chooser.set_current_folder(self.settings.lastProjectFolder) - formatter_assets = GES.list_assets(GES.Formatter) - formatter_assets.sort( - key=lambda x: - x.get_meta(GES.META_FORMATTER_RANK)) - for format_ in formatter_assets: - filt = Gtk.FileFilter() - filt.set_name(format_.get_meta(GES.META_DESCRIPTION)) - filt.add_pattern("*%s" % - format_.get_meta(GES.META_FORMATTER_EXTENSION)) - chooser.add_filter(filt) - default = Gtk.FileFilter() - default.set_name(_("All supported formats")) - default.add_custom(Gtk.FileFilterFlags.URI, self._canLoadUri, None) - chooser.add_filter(default) - - response = chooser.run() - uri = chooser.get_uri() - chooser.destroy() - if response == Gtk.ResponseType.OK: - self.app.project_manager.loadProject(uri) - else: - self.info("User cancelled loading a new project") - self.app.welcome_wizard.show() - - def _canLoadUri(self, filterinfo, unused_uri): - try: - return GES.Formatter.can_load_uri(filterinfo.uri) - except: - return False - - def _prefsCb(self, unused_action): - PreferencesDialog(self.app).run() - -# Project management callbacks - - def _projectManagerNewProjectLoadedCb(self, project_manager, project): - """Starts connecting the UI to the specified project. - - Args: - project_manager (ProjectManager): The project manager. - project (Project): The project which has been loaded. - """ - self.log("A new project has been loaded") - self._connectToProject(project) - project.pipeline.activatePositionListener() - self._setProject(project) - - self.updateTitle() - - if project_manager.disable_save is True: - # Special case: we enforce "Save as", but the normal "Save" button - # redirects to it if needed, so we still want it to be enabled: - self.save_action.set_enabled(True) - - if project.ges_timeline.props.duration != 0: - self.render_button.set_sensitive(True) - - def _projectManagerNewProjectLoadingCb(self, unused_project_manager, project): - uri = project.get_uri() - if uri: - self.recent_manager.add_item(uri) - self.log("A NEW project is loading, deactivate UI") - - def _projectManagerSaveProjectFailedCb(self, unused_project_manager, uri, exception=None): - project_filename = unquote(uri.split("/")[-1]) - dialog = Gtk.MessageDialog(transient_for=self, - modal=True, - message_type=Gtk.MessageType.ERROR, - buttons=Gtk.ButtonsType.OK, - text=_('Unable to save project "%s"') % project_filename) - if exception: - dialog.set_property("secondary-use-markup", True) - dialog.set_property("secondary-text", unquote(str(exception))) - dialog.set_transient_for(self) - dialog.run() - dialog.destroy() - self.error("failed to save project") - - def _projectManagerProjectSavedCb(self, unused_project_manager, project, uri): - # FIXME GES: Reimplement Undo/Redo - # self.app.action_log.checkpoint() - self.updateTitle() - - self.save_action.set_enabled(False) - if uri: - self.recent_manager.add_item(uri) - - if project.uri is None: - project.uri = uri + self.app.settings.mainWindowWidth = size.width + self.app.settings.mainWindowHeight = size.height - def _projectManagerClosingProjectCb(self, project_manager, project): - """Investigates whether it's possible to close the specified project. + def __delete_cb(self, unused_widget, unused_data=None): + self.app.settings.mainWindowHPanePosition = self.editor.secondhpaned.get_position() + self.app.settings.mainWindowMainHPanePosition = self.editor.mainhpaned.get_position() + self.app.settings.mainWindowVPanePosition = self.editor.toplevel_widget.get_position() - Args: - project_manager (ProjectManager): The project manager. - project (Project): The project which has been closed. - - Returns: - bool: True when it's OK to close it, False when the user chooses - to cancel the closing operation. - """ - if not project.hasUnsavedModifications(): + if not self.app.shutdown(): return True - - if project.uri and not project_manager.disable_save: - save = _("Save") - else: - save = _("Save as...") - - dialog = Gtk.Dialog(title="", transient_for=self, modal=True) - dialog.add_buttons(_("Close without saving"), Gtk.ResponseType.REJECT, - _("Cancel"), Gtk.ResponseType.CANCEL, - save, Gtk.ResponseType.YES) - # Even though we set the title to an empty string when creating dialog, - # seems we really have to do it once more so it doesn't show - # "pitivi"... - dialog.set_resizable(False) - dialog.set_default_response(Gtk.ResponseType.CANCEL) - dialog.get_accessible().set_name("unsaved changes dialog") - - primary = Gtk.Label() - primary.set_line_wrap(True) - primary.set_use_markup(True) - primary.set_alignment(0, 0.5) - - message = _("Save changes to the current project before closing?") - primary.set_markup("" + message + "") - - secondary = Gtk.Label() - secondary.set_line_wrap(True) - secondary.set_use_markup(True) - secondary.set_alignment(0, 0.5) - - if project.uri: - path = unquote(project.uri).split("file://")[1] - last_saved = max( - os.path.getmtime(path), project_manager.time_loaded) - time_delta = time() - last_saved - message = _("If you don't save, " - "the changes from the last %s will be lost.") % \ - beautify_time_delta(time_delta) - else: - message = _("If you don't save, your changes will be lost.") - secondary.props.label = message - - # put the text in a vbox - vbox = Gtk.Box(homogeneous=False, spacing=SPACING * 2) - vbox.set_orientation(Gtk.Orientation.VERTICAL) - vbox.pack_start(primary, True, True, 0) - vbox.pack_start(secondary, True, True, 0) - - # make the [[image] text] hbox - image = Gtk.Image.new_from_icon_name( - "dialog-question", Gtk.IconSize.DIALOG) - hbox = Gtk.Box(homogeneous=False, spacing=SPACING * 2) - hbox.set_orientation(Gtk.Orientation.HORIZONTAL) - hbox.pack_start(image, False, False, 0) - hbox.pack_start(vbox, True, True, 0) - hbox.set_border_width(SPACING) - - # stuff the hbox in the dialog - content_area = dialog.get_content_area() - content_area.pack_start(hbox, True, True, 0) - content_area.set_spacing(SPACING * 2) - hbox.show_all() - - response = dialog.run() - dialog.destroy() - if response == Gtk.ResponseType.YES: - if project.uri is not None and project_manager.disable_save is False: - res = self.app.project_manager.saveProject() - else: - res = self.saveProjectAs() - elif response == Gtk.ResponseType.REJECT: - res = True - else: - res = False - - return res - - def _projectManagerProjectClosedCb(self, unused_project_manager, project): - """Starts disconnecting the UI from the specified project. - - This happens when the user closes the app or asks to load another - project, immediately after the user confirmed that unsaved changes, - if any, can be discarded but before the filechooser to pick the next - project to load appears. - - Args: - project (Project): The project which has been closed. - """ - - # We must disconnect from the project pipeline before it is released: - if project.pipeline is not None: - project.pipeline.deactivatePositionListener() - - self.info("Project closed") - self.updateTitle() - if project.loaded: - self._disconnectFromProject(project) - self.timeline_ui.setProject(None) - self.render_button.set_sensitive(False) return False - def _projectManagerRevertingToSavedCb(self, unused_project_manager, unused_project): - if self.app.project_manager.current_project.hasUnsavedModifications(): - dialog = Gtk.MessageDialog(transient_for=self, - modal=True, - message_type=Gtk.MessageType.WARNING, - buttons=Gtk.ButtonsType.NONE, - text=_("Revert to saved project version?")) - dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.NO, - Gtk.STOCK_REVERT_TO_SAVED, Gtk.ResponseType.YES) - dialog.set_resizable(False) - dialog.set_property("secondary-text", - _("This will reload the current project. All unsaved changes will be lost.")) - dialog.set_default_response(Gtk.ResponseType.NO) - dialog.set_transient_for(self) - response = dialog.run() - dialog.destroy() - if response != Gtk.ResponseType.YES: - return False - return True + def __new_project_loading_cb(self, unused_project_manager, unused_project): + self.show_perspective(self.editor) - def _projectManagerNewProjectFailedCb(self, unused_project_manager, uri, reason): + def __new_project_failed_cb(self, unused_project_manager, uri, reason): project_filename = unquote(uri.split("/")[-1]) dialog = Gtk.MessageDialog(transient_for=self, modal=True, @@ -902,451 +218,22 @@ class MainWindow(Gtk.ApplicationWindow, Loggable): dialog.set_transient_for(self) dialog.run() dialog.destroy() - self.app.welcome_wizard.show() - - def _projectManagerMissingUriCb(self, project_manager, project, unused_error, asset): - if project.at_least_one_asset_missing: - # One asset is already missing so no point in spamming the user - # with more file-missing dialogs, as we need all of them. - return None - - if self.app.proxy_manager.is_proxy_asset(asset): - uri = self.app.proxy_manager.getTargetUri(asset) - else: - uri = asset.get_id() - dialog = Gtk.Dialog(title=_("Locate missing file..."), - transient_for=self, - modal=True) - - dialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Open"), Gtk.ResponseType.OK) - dialog.set_border_width(SPACING * 2) - dialog.get_content_area().set_spacing(SPACING) - dialog.set_transient_for(self) - dialog.set_default_response(Gtk.ResponseType.OK) - - # This box will contain widgets with details about the missing file. - vbox = Gtk.Box() - vbox.set_orientation(Gtk.Orientation.VERTICAL) - - label_start = Gtk.Label() - label_start.set_markup(_("The following file could not be found:")) - label_start.set_xalign(0) - vbox.pack_start(label_start, False, False, 0) - - hbox = Gtk.Box() - hbox.set_orientation(Gtk.Orientation.HORIZONTAL) - hbox.set_margin_top(PADDING) - hbox.set_spacing(PADDING * 2) - - label_asset_info = Gtk.Label() - label_asset_info.set_markup(beautify_missing_asset(asset)) - label_asset_info.set_xalign(0) - label_asset_info.set_yalign(0) - hbox.pack_start(label_asset_info, False, False, 0) - - small_thumb, large_thumb = AssetThumbnail.get_thumbnails_from_xdg_cache(uri) - if large_thumb: - self.debug("A thumbnail file was found for %s", uri) - thumbnail = Gtk.Image.new_from_pixbuf(large_thumb) - hbox.pack_end(thumbnail, False, False, 0) - - vbox.pack_start(hbox, False, False, 0) - - label_end = Gtk.Label() - label_end.set_markup(_("Please specify its new location:")) - label_end.set_xalign(0) - label_end.set_margin_top(PADDING) - vbox.pack_start(label_end, False, False, 0) - - dialog.get_content_area().pack_start(vbox, False, False, 0) - vbox.show_all() - - chooser = Gtk.FileChooserWidget(action=Gtk.FileChooserAction.OPEN) - chooser.set_select_multiple(False) - previewer = PreviewWidget(self.settings, discover_sync=True) - chooser.set_preview_widget(previewer) - chooser.set_use_preview_label(False) - chooser.connect('update-preview', previewer.update_preview_cb) - chooser.set_current_folder(self.settings.lastProjectFolder) - # Use a Gtk FileFilter to only show files with the same extension - # Note that splitext gives us the extension with the ".", no need to - # add it inside the filter string. - unused_filename, extension = os.path.splitext(uri) - filter_ = Gtk.FileFilter() - # Translators: this is a format filter in a filechooser. Ex: "AVI - # files" - filter_.set_name(_("%s files") % extension) - filter_.add_pattern("*%s" % extension.lower()) - filter_.add_pattern("*%s" % extension.upper()) - default = Gtk.FileFilter() - default.set_name(_("All files")) - default.add_pattern("*") - chooser.add_filter(filter_) - chooser.add_filter(default) - dialog.get_content_area().pack_start(chooser, True, True, 0) - chooser.show() - - # If the window is too big, the window manager will resize it so that - # it fits on the screen. - dialog.set_default_size(1024, 1000) - response = dialog.run() - - new_uri = None - if response == Gtk.ResponseType.OK: - self.log("User chose a new URI for the missing file") - new_uri = chooser.get_uri() - else: - dialog.hide() - - if not self.app.proxy_manager.checkProxyLoadingSucceeded(asset): - # Reset the project manager and disconnect all the signals. - project_manager.closeRunningProject() - # Signal the project loading failure. - # You have to do this *after* successfully creating a blank project, - # or the startupwizard will still be connected to that signal too. - reason = _('No replacement file was provided for "%s".\n\n' - 'Pitivi does not currently support partial projects.') % \ - info_name(asset) - project_manager.emit("new-project-failed", project.uri, reason) - - dialog.destroy() - return new_uri - - def _connectToProject(self, project): - # FIXME GES we should re-enable this when possible - # medialibrary.connect("missing-plugins", self._sourceListMissingPluginsCb) - project.connect("project-changed", self._projectChangedCb) - project.connect("rendering-settings-changed", - self._rendering_settings_changed_cb) - project.ges_timeline.connect("notify::duration", - self._timelineDurationChangedCb) - - def _sourceListMissingPluginsCb( - self, unused_project, unused_uri, unused_factory, - details, unused_descriptions, missingPluginsCallback): - res = self._installPlugins(details, missingPluginsCallback) - return res - - def _installPlugins(self, details, missingPluginsCallback): - context = GstPbutils.InstallPluginsContext() - if self.app.system.has_x11(): - context.set_xid(self.window.xid) - - res = GstPbutils.install_plugins_async(details, context, - missingPluginsCallback) - return res + self.show_perspective(self.greeter) - def _setProject(self, project): - """Disconnects and then reconnects callbacks to the specified project. + def __project_closed_cb(self, unused_project_manager, unused_project): + self.show_perspective(self.greeter) - Args: - project (Project): The new current project. - """ - if not project: - self.warning("Current project instance does not exist") - return False - - self.viewer.setPipeline(project.pipeline) - self._reset_viewer_aspect_ratio(project) - self.clipconfig.project = project - - # When creating a blank project there's no project URI yet. - if project.uri: - folder_path = os.path.dirname(path_from_uri(project.uri)) - self.settings.lastProjectFolder = folder_path - - def _disconnectFromProject(self, project): - project.disconnect_by_func(self._projectChangedCb) - project.disconnect_by_func(self._rendering_settings_changed_cb) - project.ges_timeline.disconnect_by_func(self._timelineDurationChangedCb) - - def _rendering_settings_changed_cb(self, project, unused_item): - """Handles Project metadata changes.""" - self._reset_viewer_aspect_ratio(project) - - def _reset_viewer_aspect_ratio(self, project): - """Resets the viewer aspect ratio.""" - self.viewer.setDisplayAspectRatio(project.getDAR()) - self.viewer.timecode_entry.setFramerate(project.videorate) - - def _timelineDurationChangedCb(self, timeline, unused_duration): - """Updates the render button. - - This covers the case when a clip is inserted into a blank timeline. - This callback is not triggered by loading a project. - """ - duration = timeline.get_duration() - self.debug("Timeline duration changed to %s", duration) - self.render_button.set_sensitive(duration > 0) - - def _showExportDialog(self, project): - self.log("Export requested") - chooser = Gtk.FileChooserDialog(title=_("Export To..."), - transient_for=self, - action=Gtk.FileChooserAction.SAVE) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Save"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - - chooser.set_select_multiple(False) - chooser.props.do_overwrite_confirmation = True - - asset = GES.Formatter.get_default() - asset_extension = asset.get_meta(GES.META_FORMATTER_EXTENSION) - - if not project.name: - chooser.set_current_name( - _("Untitled") + "." + asset_extension + "_tar") - else: - chooser.set_current_name( - project.name + "." + asset_extension + "_tar") - - filt = Gtk.FileFilter() - filt.set_name(_("Tar archive")) - filt.add_pattern("*.%s_tar" % asset_extension) - chooser.add_filter(filt) - default = Gtk.FileFilter() - default.set_name(_("Detect automatically")) - default.add_pattern("*") - chooser.add_filter(default) - - response = chooser.run() - if response == Gtk.ResponseType.OK: - self.log("User chose a URI to export project to") - # need to do this to work around bug in Gst.uri_construct - # which escapes all /'s in path! - uri = "file://" + chooser.get_filename() - self.log("uri: %s", uri) - ret = uri - else: - self.log("User didn't choose a URI to export project to") - ret = None - - chooser.destroy() - return ret - - def saveProjectAs(self): - uri = self._showSaveAsDialog() - if uri is None: - return False - return self.app.project_manager.saveProject(uri) - - def _showSaveAsDialog(self): - self.log("Save URI requested") - chooser = Gtk.FileChooserDialog(title=_("Save As..."), - transient_for=self, - action=Gtk.FileChooserAction.SAVE) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Save"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - asset = GES.Formatter.get_default() - filt = Gtk.FileFilter() - filt.set_name(asset.get_meta(GES.META_DESCRIPTION)) - filt.add_pattern("*.%s" % asset.get_meta(GES.META_FORMATTER_EXTENSION)) - chooser.add_filter(filt) - - chooser.set_select_multiple(False) - chooser.set_current_name(_("Untitled") + "." + - asset.get_meta(GES.META_FORMATTER_EXTENSION)) - chooser.set_current_folder(self.settings.lastProjectFolder) - chooser.props.do_overwrite_confirmation = True - - default = Gtk.FileFilter() - default.set_name(_("Detect automatically")) - default.add_pattern("*") - chooser.add_filter(default) - - response = chooser.run() - if response == Gtk.ResponseType.OK: - self.log("User chose a URI to save project to") - # need to do this to work around bug in Gst.uri_construct - # which escapes all /'s in path! - uri = "file://" + chooser.get_filename() - file_filter = chooser.get_filter().get_name() - self.log("uri:%s , filter:%s", uri, file_filter) - self.settings.lastProjectFolder = chooser.get_current_folder() - ret = uri - else: - self.log("User didn't choose a URI to save project to") - ret = None - - chooser.destroy() - return ret - - def _screenshotCb(self, unused_action): - """Exports a snapshot of the current frame as an image file.""" - foo = self._showSaveScreenshotDialog() - if foo: - path, mime = foo[0], foo[1] - self.app.project_manager.current_project.pipeline.save_thumbnail( - -1, -1, mime, path) - - def _showSaveScreenshotDialog(self): - """Asks the user where to save the current frame. - - Returns: - List[str]: The full path and the mimetype if successful, None otherwise. - """ - chooser = Gtk.FileChooserDialog(title=_("Save As..."), - transient_for=self, action=Gtk.FileChooserAction.SAVE) - chooser.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL, - _("Save"), Gtk.ResponseType.OK) - chooser.set_default_response(Gtk.ResponseType.OK) - chooser.set_select_multiple(False) - chooser.set_current_name(_("Untitled")) - chooser.props.do_overwrite_confirmation = True - formats = {_("PNG image"): ["image/png", ("png",)], - _("JPEG image"): ["image/jpeg", ("jpg", "jpeg")]} - for format in formats: - filt = Gtk.FileFilter() - filt.set_name(format) - filt.add_mime_type(formats.get(format)[0]) - chooser.add_filter(filt) - response = chooser.run() - if response == Gtk.ResponseType.OK: - chosen_format = formats.get(chooser.get_filter().get_name()) - chosen_ext = chosen_format[1][0] - chosen_mime = chosen_format[0] - uri = os.path.join( - chooser.get_current_folder(), chooser.get_filename()) - ret = ["%s.%s" % (uri, chosen_ext), chosen_mime] - else: - ret = None - chooser.destroy() - return ret - - def updateTitle(self): - project = self.app.project_manager.current_project - if project: - if project.name: - name = project.name - else: - name = _("Untitled") - unsaved_mark = "" - if project.hasUnsavedModifications(): - unsaved_mark = "*" - title = "%s%s — %s" % (unsaved_mark, name, APPNAME) - else: - title = APPNAME - event_box = Gtk.EventBox() - label = Gtk.Label() - clear_styles(label) - label.set_text(title) - event_box.add(label) - event_box.show_all() - event_box.connect("button-press-event", self.__titleClickCb, project) - self._headerbar.set_custom_title(event_box) - self.set_title(title) - - def __titleClickCb(self, unused_widget, unused_event, project): - entry = Gtk.Entry() - entry.set_width_chars(100) - entry.set_margin_left(SPACING) - entry.set_margin_right(SPACING) - entry.show() - entry.set_text(project.name) - self._headerbar.set_custom_title(entry) - if project.hasDefaultName(): - entry.grab_focus() - else: - entry.grab_focus_without_selecting() - entry.connect("focus-out-event", self.__titleChangedCb, project) - entry.connect("key_release_event", self.__titleTypeCb, project) - - def __titleChangedCb(self, widget, event, project): - if not event.window: - # Workaround https://bugzilla.gnome.org/show_bug.cgi?id=757036 + def show_perspective(self, perspective): + """Displays the specified perspective.""" + if self.__perspective is perspective: return - name = widget.get_text() - if project.name == name: - self.updateTitle() - else: - project.name = name - - def __titleTypeCb(self, widget, event, project): - if event.keyval == Gdk.KEY_Return: - self.focusTimeline() - return True - elif event.keyval == Gdk.KEY_Escape: - widget.set_text(project.name) - self.focusTimeline() - return True - return False - - -class PreviewAssetWindow(Gtk.Window): - """Window for previewing a video or audio asset. - - Args: - asset (GES.UriClipAsset): The asset to be previewed. - main_window (MainWindow): The main window. - """ - - def __init__(self, asset, main_window): - Gtk.Window.__init__(self) - self._asset = asset - self._main_window = main_window - - self.set_title(_("Preview")) - self.set_type_hint(Gdk.WindowTypeHint.UTILITY) - self.set_transient_for(main_window) - - self._previewer = PreviewWidget(main_window.settings, minimal=True) - self.add(self._previewer) - self._previewer.preview_uri(self._asset.get_id()) - self._previewer.show() - - self.connect("focus-out-event", self._leavePreviewCb) - self.connect("key-press-event", self._keyPressCb) - - def preview(self): - """Shows the window and starts the playback.""" - width, height = self._calculatePreviewWindowSize() - self.resize(width, height) - # Setting the position of the window only works if it's currently hidden - # otherwise, after the resize the position will not be readjusted - self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) - self.show() - - self._previewer.play() - # Hack so that we really really force the "utility" window to be - # focused - self.present() - - def _calculatePreviewWindowSize(self): - info = self._asset.get_info() - video_streams = info.get_video_streams() - if not video_streams: - # There is no video/image stream. This is an audio file. - # Resize to the minimum and let the window manager deal with it. - return 1, 1 - # For videos and images, automatically resize the window - # Try to keep it 1:1 if it can fit within 85% of the parent window - video = video_streams[0] - img_width = video.get_square_width() - img_height = video.get_height() - mainwindow_width, mainwindow_height = self._main_window.get_size() - max_width = 0.85 * mainwindow_width - max_height = 0.85 * mainwindow_height - - controls_height = self._previewer.bbox.get_preferred_size()[0].height - if img_width < max_width and (img_height + controls_height) < max_height: - # The video is small enough, keep it 1:1 - return img_width, img_height + controls_height - else: - # The video is too big, size it down - # TODO: be smarter, figure out which (width, height) is bigger - new_height = max_width * img_height / img_width - return int(max_width), int(new_height + controls_height) - - def _leavePreviewCb(self, window, unused): - self.destroy() - return True - - def _keyPressCb(self, unused_widget, event): - if event.keyval in (Gdk.KEY_Escape, Gdk.KEY_Q, Gdk.KEY_q): - self.destroy() - elif event.keyval == Gdk.KEY_space: - self._previewer.togglePlayback() - return True + if self.__perspective: + # Remove the current perspective before adding the + # specified perspective because we can only add one + # toplevel widget to the main window at a time. + self.remove(self.__perspective.toplevel_widget) + self.log("Displaying perspective: %s", type(perspective).__name__) + self.__perspective = perspective + self.set_titlebar(perspective.headerbar) + self.add(perspective.toplevel_widget) + perspective.refresh() diff --git a/pitivi/medialibrary.py b/pitivi/medialibrary.py index 9af5968cc..e5843e2a8 100644 --- a/pitivi/medialibrary.py +++ b/pitivi/medialibrary.py @@ -655,18 +655,18 @@ class MediaLibraryWidget(Gtk.Box, Loggable): asset = model[row.get_path()][COL_ASSET] target = asset.get_proxy_target() self._project.remove_asset(asset) - self.app.gui.timeline_ui.purgeAsset(asset.props.id) + self.app.gui.editor.timeline_ui.purgeAsset(asset.props.id) if target: self._project.remove_asset(target) - self.app.gui.timeline_ui.purgeAsset(target.props.id) + self.app.gui.editor.timeline_ui.purgeAsset(target.props.id) # The treeview can make some of the remaining items selected, so # make sure none are selected. self._unselectAll() def _insertEndCb(self, unused_action, unused_parameter): - self.app.gui.timeline_ui.insertAssets(self.getSelectedAssets(), -1) + self.app.gui.editor.timeline_ui.insertAssets(self.getSelectedAssets(), -1) def _searchEntryChangedCb(self, entry): # With many hundred clips in an iconview with dynamic columns and @@ -877,7 +877,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable): self._addAsset(asset) if self._project.loaded: - self.app.gui.timeline_ui.switchProxies(asset) + self.app.gui.editor.timeline_ui.switchProxies(asset) def _assetAddedCb(self, unused_project, asset): """Checks whether the asset added to the project should be shown.""" @@ -1045,7 +1045,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable): def __projectSettingsSetInfobarCb(self, infobar, response_id): if response_id == Gtk.ResponseType.OK: - self.app.gui.showProjectSettingsDialog() + self.app.gui.editor.showProjectSettingsDialog() infobar.hide() def _clipPropertiesCb(self, unused_widget): diff --git a/pitivi/perspective.py b/pitivi/perspective.py new file mode 100644 index 000000000..4496a1dff --- /dev/null +++ b/pitivi/perspective.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Pitivi video editor +# Copyright (c) 2018 Harish Fulara +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, +# Boston, MA 02110-1301, USA. +"""Interface for different perspectives.""" + + +class Perspective(object): + """Interface for different perspectives.""" + + def __init__(self): + self.toplevel_widget = None + self.headerbar = None + self.menu_button = None + + def setup_ui(self): + """Sets up the UI of the perspective. + + Populates the toplevel_widget, headerbar and menu_button attributes. + """ + raise NotImplementedError() + + def refresh(self): + """Refreshes the perspective while activating it.""" + raise NotImplementedError() diff --git a/pitivi/project.py b/pitivi/project.py index 5c32d470c..1fb84096a 100644 --- a/pitivi/project.py +++ b/pitivi/project.py @@ -213,9 +213,9 @@ class ProjectManager(GObject.Object, Loggable): dialog.destroy() if response == 1: - self.app.gui.saveProjectAsDialog() + self.app.gui.editor.saveProjectAsDialog() elif response == 2: - self.app.gui.saveProject() + self.app.gui.editor.saveProject() self.app.shutdown() @@ -225,8 +225,7 @@ class ProjectManager(GObject.Object, Loggable): If a backup file exists, asks if it should be loaded instead, and if so, forces the user to use "Save as" afterwards. """ - if self.current_project is not None and not self.closeRunningProject(): - return False + assert self.current_project is None is_validate_scenario = self._isValidateScenario(uri) if not is_validate_scenario: @@ -509,11 +508,8 @@ class ProjectManager(GObject.Object, Loggable): bool: Whether the project has been created successfully. """ self.debug("New blank project") - if self.current_project is not None: - # This will prompt users about unsaved changes (if any): - if not ignore_unsaved_changes and not self.closeRunningProject(): - # The user has not made a decision, don't do anything - return False + + assert self.current_project is None self.__start_loading_time = time.time() project = Project(self.app, name=DEFAULT_NAME) diff --git a/pitivi/render.py b/pitivi/render.py index 3052c4869..f902a2f07 100644 --- a/pitivi/render.py +++ b/pitivi/render.py @@ -1006,7 +1006,7 @@ class RenderDialog(Loggable): # Hide the rendering settings dialog while rendering self.window.hide() - self.app.gui.timeline_ui.timeline.set_best_zoom_ratio(allow_zoom_in=True) + self.app.gui.editor.timeline_ui.timeline.set_best_zoom_ratio(allow_zoom_in=True) self.project.set_rendering(True) self._pipeline.set_render_settings( self.outfile, self.project.container_profile) diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py index 6b6bb3990..98cb4d14f 100644 --- a/pitivi/timeline/elements.py +++ b/pitivi/timeline/elements.py @@ -1129,7 +1129,7 @@ class Clip(Gtk.EventBox, Zoomable, Loggable): self.timeline.resetSelectionGroup() self.timeline.current_group.add(self.ges_clip) self.timeline.selection.setSelection([self.ges_clip], SELECT) - self.app.gui.switchContextTab(self.ges_clip) + self.app.gui.editor.switchContextTab(self.ges_clip) effect_info = self.app.effects.getInfo(self.timeline.dropData) pipeline = self.timeline.ges_timeline.get_parent() @@ -1270,7 +1270,7 @@ class Clip(Gtk.EventBox, Zoomable, Loggable): else: self.timeline.resetSelectionGroup() self.timeline.current_group.add(self.ges_clip.get_toplevel_parent()) - self.app.gui.switchContextTab(self.ges_clip) + self.app.gui.editor.switchContextTab(self.ges_clip) parent = self.ges_clip.get_parent() if parent == self.timeline.current_group or parent is None: @@ -1474,9 +1474,9 @@ class TransitionClip(Clip): def _selectedChangedCb(self, unused_selected, selected, ges_timeline_element): if selected: - self.app.gui.trans_list.activate(ges_timeline_element) + self.app.gui.editor.trans_list.activate(ges_timeline_element) else: - self.app.gui.trans_list.deactivate() + self.app.gui.editor.trans_list.deactivate() GES_TYPE_UI_TYPE = { diff --git a/pitivi/timeline/ruler.py b/pitivi/timeline/ruler.py index 40cc9b00b..d8fc327d8 100644 --- a/pitivi/timeline/ruler.py +++ b/pitivi/timeline/ruler.py @@ -206,7 +206,7 @@ class ScaleRuler(Gtk.DrawingArea, Zoomable, Loggable): button = event.button if button == 3 or (button == 1 and self.app.settings.leftClickAlsoSeeks): self.debug("button released at x:%d", event.x) - self.app.gui.focusTimeline() + self.app.gui.editor.focusTimeline() position = self.pixelToNs(event.x + self.pixbuf_offset) self.__set_tooltip_text(position) return False diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py index eab4cbfde..94db59830 100644 --- a/pitivi/timeline/timeline.py +++ b/pitivi/timeline/timeline.py @@ -639,7 +639,7 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): def update_visible_overlays(self): sources = self.get_sources_at_position(self.__last_position) - self.app.gui.viewer.overlay_stack.set_current_sources(sources) + self.app.gui.editor.viewer.overlay_stack.set_current_sources(sources) def set_next_seek_position(self, next_seek_position): """Sets the position the playhead seeks to on the next button-release. @@ -651,7 +651,7 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable): def _button_press_event_cb(self, unused_widget, event): self.debug("PRESSED %s", event) - self.app.gui.focusTimeline() + self.app.gui.editor.focusTimeline() event_widget = Gtk.get_event_widget(event) @@ -1411,7 +1411,7 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable): else: raise TimelineError("Cannot insert: %s" % type(obj)) clip_position += duration - self.app.gui.focusTimeline() + self.app.gui.editor.focusTimeline() if zoom_was_fitted: self.timeline.set_best_zoom_ratio() diff --git a/pitivi/titleeditor.py b/pitivi/titleeditor.py index 1c8996420..c6be684b7 100644 --- a/pitivi/titleeditor.py +++ b/pitivi/titleeditor.py @@ -225,7 +225,7 @@ class TitleEditor(Loggable): title_clip = GES.TitleClip() duration = self.app.settings.titleClipLength * Gst.MSECOND title_clip.set_duration(duration) - self.app.gui.timeline_ui.insert_clips_on_first_layer([title_clip]) + self.app.gui.editor.timeline_ui.insert_clips_on_first_layer([title_clip]) # Now that the clip is inserted in the timeline, it has a source which # can be used to set its properties. source = title_clip.get_children(False)[0] @@ -237,8 +237,8 @@ class TitleEditor(Loggable): assert source.set_child_property("halignment", DEFAULT_HALIGNMENT) # Select it so the Title editor becomes active. self._selection.setSelection([title_clip], SELECT) - self.app.gui.timeline_ui.timeline.resetSelectionGroup() - self.app.gui.timeline_ui.timeline.current_group.add(title_clip) + self.app.gui.editor.timeline_ui.timeline.resetSelectionGroup() + self.app.gui.editor.timeline_ui.timeline.current_group.add(title_clip) def _propertyChangedCb(self, source, unused_gstelement, pspec): if self._setting_props: diff --git a/pitivi/utils/pipeline.py b/pitivi/utils/pipeline.py index ddafd6ef6..d2a622ee8 100644 --- a/pitivi/utils/pipeline.py +++ b/pitivi/utils/pipeline.py @@ -613,7 +613,7 @@ class Pipeline(GES.Pipeline, SimplePipeline): def _busMessageCb(self, bus, message): if message.type == Gst.MessageType.ASYNC_DONE: - self.app.gui.timeline_ui.timeline.update_visible_overlays() + self.app.gui.editor.timeline_ui.timeline.update_visible_overlays() if message.type == Gst.MessageType.ASYNC_DONE and\ self._commit_wanted: diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py index d17a124c6..d4d2e9054 100644 --- a/pitivi/utils/timeline.py +++ b/pitivi/utils/timeline.py @@ -265,7 +265,7 @@ class EditingContext(GObject.Object, Loggable): if self.__log_actions: self.app.action_log.commit("move-clip") self.timeline.get_asset().pipeline.commit_timeline() - self.timeline.ui.app.gui.viewer.clipTrimPreviewFinished() + self.timeline.ui.app.gui.editor.viewer.clipTrimPreviewFinished() def setMode(self, mode): """Sets the current editing mode. @@ -302,10 +302,11 @@ class EditingContext(GObject.Object, Loggable): if res and self.mode == GES.EditMode.EDIT_TRIM and self.with_video: if self.edge == GES.Edge.EDGE_START: - self.timeline.ui.app.gui.viewer.clipTrimPreview(self.focus, self.focus.props.in_point) + self.timeline.ui.app.gui.editor.viewer.clipTrimPreview( + self.focus, self.focus.props.in_point) elif self.edge == GES.Edge.EDGE_END: - self.timeline.ui.app.gui.viewer.clipTrimPreview(self.focus, - self.focus.props.duration + self.focus.props.in_point) + self.timeline.ui.app.gui.editor.viewer.clipTrimPreview( + self.focus, self.focus.props.duration + self.focus.props.in_point) # -------------------------- Interfaces ----------------------------------------# diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py index 1461b489d..857d49c3d 100644 --- a/pitivi/utils/ui.py +++ b/pitivi/utils/ui.py @@ -95,6 +95,32 @@ NORMAL_FONT = _get_font("font-name", "Cantarell") DOCUMENT_FONT = _get_font("document-font-name", "Sans") MONOSPACE_FONT = _get_font("monospace-font-name", "Monospace") +GREETER_PERSPECTIVE_CSS = """ + #recent_projects_listbox { + font-weight: bold; + border: 1px solid alpha(@borders, 0.6); + } + + #recent_projects_listbox row { + padding: 10px 200px 10px 10px; + border-bottom: 1px solid alpha(@borders, 0.2); + } + + #recent_projects_listbox row:last-child { + border-bottom-width: 0px; + } + + #recent_projects_labelbox { + color: @insensitive_fg_color; + font-weight: bold; + padding-bottom: 6px; + } + + #recent_projects_labelbox > label:backdrop { + color: @unfocused_insensitive_color; + } +""" + TIMELINE_CSS = """ .AudioBackground { background-color: #496c21; diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py index 57c4d5ea8..fc04d1e21 100644 --- a/pitivi/utils/widgets.py +++ b/pitivi/utils/widgets.py @@ -1310,20 +1310,20 @@ class BaseTabs(Gtk.Notebook): return notebook def _hideSecondHpanedInMainWindow(self): - self.app.gui.mainhpaned.remove(self.app.gui.secondhpaned) - self.app.gui.secondhpaned.remove(self.app.gui.projecttabs) - self.app.gui.secondhpaned.remove(self.app.gui.propertiestabs) - self.app.gui.mainhpaned.pack1(self.app.gui.projecttabs, resize=True, - shrink=False) + self.app.gui.editor.mainhpaned.remove(self.app.gui.editor.secondhpaned) + self.app.gui.editor.secondhpaned.remove(self.app.gui.editor.projecttabs) + self.app.gui.editor.secondhpaned.remove(self.app.gui.editor.propertiestabs) + self.app.gui.editor.mainhpaned.pack1(self.app.gui.editor.projecttabs, + resize=True, shrink=False) def _showSecondHpanedInMainWindow(self): - self.app.gui.mainhpaned.remove(self.app.gui.projecttabs) - self.app.gui.secondhpaned.pack1(self.app.gui.projecttabs, - resize=True, shrink=False) - self.app.gui.secondhpaned.pack2(self.app.gui.propertiestabs, - resize=True, shrink=False) - self.app.gui.mainhpaned.pack1(self.app.gui.secondhpaned, - resize=True, shrink=False) + self.app.gui.editor.mainhpaned.remove(self.app.gui.editor.projecttabs) + self.app.gui.editor.secondhpaned.pack1(self.app.gui.editor.projecttabs, + resize=True, shrink=False) + self.app.gui.editor.secondhpaned.pack2(self.app.gui.editor.propertiestabs, + resize=True, shrink=False) + self.app.gui.editor.mainhpaned.pack1(self.app.gui.editor.secondhpaned, + resize=True, shrink=False) class ZoomBox(Gtk.Grid, Zoomable): diff --git a/pitivi/viewer/overlay.py b/pitivi/viewer/overlay.py index 43086d26d..74fa5d2cc 100644 --- a/pitivi/viewer/overlay.py +++ b/pitivi/viewer/overlay.py @@ -55,7 +55,7 @@ class Overlay(Gtk.DrawingArea, Loggable): def _select(self): self.stack.selected_overlay = self - self.stack.app.gui.timeline_ui.timeline.selection.setSelection([self._source], SELECT) + self.stack.app.gui.editor.timeline_ui.timeline.selection.setSelection([self._source], SELECT) if isinstance(self._source, GES.TitleSource): page = 2 elif isinstance(self._source, GES.VideoUriSource): @@ -63,7 +63,7 @@ class Overlay(Gtk.DrawingArea, Loggable): else: self.warning("Unknown clip type: %s", self._source) return - self.stack.app.gui.context_tabs.set_current_page(page) + self.stack.app.gui.editor.context_tabs.set_current_page(page) def __source_selected_changed_cb(self, unused_source, selected): if not selected and self._is_selected(): diff --git a/pitivi/viewer/viewer.py b/pitivi/viewer/viewer.py index 849404242..9a14aee9c 100644 --- a/pitivi/viewer/viewer.py +++ b/pitivi/viewer/viewer.py @@ -291,12 +291,13 @@ class ViewerContainer(Gtk.Box, Loggable): def _entryActivateCb(self, unused_entry): nanoseconds = self.timecode_entry.getWidgetValue() self.app.project_manager.current_project.pipeline.simple_seek(nanoseconds) - self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.CENTER, when_not_in_view=True) + self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead( + align=Gtk.Align.CENTER, when_not_in_view=True) def _entry_key_press_event_cb(self, widget, event): """Handles the key press events in the timecode_entry widget.""" if event.keyval == Gdk.KEY_Escape: - self.app.gui.focusTimeline() + self.app.gui.editor.focusTimeline() # Active Timeline calllbacks def _durationChangedCb(self, unused_pipeline, duration): @@ -307,30 +308,34 @@ class ViewerContainer(Gtk.Box, Loggable): def _playButtonCb(self, unused_button, unused_playing): self.app.project_manager.current_project.pipeline.togglePlayback() - self.app.gui.focusTimeline() + self.app.gui.editor.focusTimeline() def _goToStartCb(self, unused_button): self.app.project_manager.current_project.pipeline.simple_seek(0) - self.app.gui.focusTimeline() - self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.START, when_not_in_view=True) + self.app.gui.editor.focusTimeline() + self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead( + align=Gtk.Align.START, when_not_in_view=True) def _backCb(self, unused_button): # Seek backwards one second self.app.project_manager.current_project.pipeline.seekRelative(0 - Gst.SECOND) - self.app.gui.focusTimeline() - self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.END, when_not_in_view=True) + self.app.gui.editor.focusTimeline() + self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead( + align=Gtk.Align.END, when_not_in_view=True) def _forwardCb(self, unused_button): # Seek forward one second self.app.project_manager.current_project.pipeline.seekRelative(Gst.SECOND) - self.app.gui.focusTimeline() - self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.START, when_not_in_view=True) + self.app.gui.editor.focusTimeline() + self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead( + align=Gtk.Align.START, when_not_in_view=True) def _goToEndCb(self, unused_button): end = self.app.project_manager.current_project.pipeline.getDuration() self.app.project_manager.current_project.pipeline.simple_seek(end) - self.app.gui.focusTimeline() - self.app.gui.timeline_ui.timeline.scrollToPlayhead(align=Gtk.Align.CENTER, when_not_in_view=True) + self.app.gui.editor.focusTimeline() + self.app.gui.editor.timeline_ui.timeline.scrollToPlayhead( + align=Gtk.Align.CENTER, when_not_in_view=True) # Public methods for controlling playback diff --git a/pre-commit.hook b/pre-commit.hook index 43a14e1f8..91e8961ec 100755 --- a/pre-commit.hook +++ b/pre-commit.hook @@ -20,8 +20,8 @@ pitivi/dialogs/depsmanager.py pitivi/dialogs/filelisterrordialog.py pitivi/dialogs/prefs.py pitivi/dialogs/startupwizard.py +pitivi/editorperspective.py pitivi/effects.py -pitivi/mainwindow.py pitivi/mediafilespreviewer.py pitivi/medialibrary.py pitivi/preset.py diff --git a/tests/test_mainwindow.py b/tests/test_editorperspective.py similarity index 76% rename from tests/test_mainwindow.py rename to tests/test_editorperspective.py index bebf93583..ca641a3fe 100644 --- a/tests/test_mainwindow.py +++ b/tests/test_editorperspective.py @@ -16,54 +16,53 @@ # License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301, USA. -""" -Tests for pitivi/mainwindow.py -""" +"""Tests for pitivi/editorperspective.py""" from unittest import mock from gi.repository import GES from gi.repository import Gtk -from pitivi.mainwindow import MainWindow +from pitivi.editorperspective import EditorPerspective from pitivi.project import ProjectManager from pitivi.utils.misc import disconnectAllByFunc from tests import common -class TestMainWindow(common.TestCase): - """Tests for the MainWindow class.""" +class TestEditorPerspective(common.TestCase): + """Tests for the EditorPerspective class.""" + # pylint: disable=protected-access def test_switch_context_tab(self): """Checks tab switches.""" app = common.create_pitivi_mock() - mainwindow = MainWindow(app) + editorperspective = EditorPerspective(app) + editorperspective.setup_ui() for expected_tab, b_element in [ (2, GES.TitleClip()), (0, GES.SourceClip()), (1, GES.TransitionClip())]: - mainwindow.switchContextTab(b_element) - self.assertEqual(mainwindow.context_tabs.get_current_page(), + editorperspective.switchContextTab(b_element) + self.assertEqual(editorperspective.context_tabs.get_current_page(), expected_tab, b_element) # Make sure the tab does not change when using an invalid argument. - mainwindow.switchContextTab("invalid") - self.assertEqual(mainwindow.context_tabs.get_current_page(), + editorperspective.switchContextTab("invalid") + self.assertEqual(editorperspective.context_tabs.get_current_page(), expected_tab) - mainwindow.destroy() - def __loading_failure(self, has_proxy): mainloop = common.create_main_loop() app = common.create_pitivi_mock(lastProjectFolder="/tmp", edgeSnapDeadband=32) app.project_manager = ProjectManager(app) - mainwindow = MainWindow(app) - mainwindow.viewer = mock.MagicMock() + editorperspective = EditorPerspective(app) + editorperspective.setup_ui() + editorperspective.viewer = mock.MagicMock() def __pm_missing_uri_cb(project_manager, project, error, asset): nonlocal mainloop - nonlocal mainwindow + nonlocal editorperspective nonlocal self nonlocal app nonlocal has_proxy @@ -81,7 +80,7 @@ class TestMainWindow(common.TestCase): app.proxy_manager.checkProxyLoadingSucceeded = \ mock.MagicMock(return_value=has_proxy) - mainwindow._projectManagerMissingUriCb( + editorperspective._projectManagerMissingUriCb( project_manager, project, error, asset) self.assertTrue(dialog.called) @@ -90,19 +89,13 @@ class TestMainWindow(common.TestCase): # pylint: disable=protected-access app.project_manager.connect("missing-uri", - mainwindow._projectManagerMissingUriCb) - # pylint: disable=protected-access - app.project_manager.connect("new-project-failed", - mainwindow._projectManagerNewProjectFailedCb) + editorperspective._projectManagerMissingUriCb) - mainwindow.destroy() mainloop.quit() # pylint: disable=protected-access disconnectAllByFunc(app.project_manager, - mainwindow._projectManagerMissingUriCb) - disconnectAllByFunc(app.project_manager, - mainwindow._projectManagerNewProjectFailedCb) + editorperspective._projectManagerMissingUriCb) app.project_manager.connect("missing-uri", __pm_missing_uri_cb) diff --git a/tests/test_project.py b/tests/test_project.py index 67485702b..258bc856b 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -85,25 +85,6 @@ class TestProjectManager(common.TestCase): signalUri, unused_message = args self.assertEqual(uri, signalUri, self.signals) - def testLoadProjectClosesCurrent(self): - """ - Check that new-project-failed is emitted if we can't close the current - project instance. - """ - state = {"tried-close": False} - - def close(): - state["tried-close"] = True - return False - self.manager.closeRunningProject = close - - uri = "file:///Untitled.xptv" - self.manager.current_project = mock.Mock() - self.manager.loadProject(uri) - - self.assertEqual(0, len(self.signals)) - self.assertTrue(state["tried-close"], self.signals) - def testLoadProject(self): self.manager.newBlankProject() @@ -190,18 +171,6 @@ class TestProjectManager(common.TestCase): self.assertTrue(self.manager.current_project is None) - def testNewBlankProjectCantCloseCurrent(self): - def closing(manager, project): - return False - - self.manager.current_project = mock.Mock() - self.manager.current_project.uri = "file:///ciao" - self.manager.connect("closing-project", closing) - self.assertFalse(self.manager.newBlankProject()) - self.assertEqual(1, len(self.signals)) - signal, args = self.signals[0] - self.assertEqual("closing-project", signal) - def testNewBlankProject(self): self.assertTrue(self.manager.newBlankProject()) self.assertEqual(3, len(self.signals)) @@ -388,6 +357,7 @@ class TestProjectLoading(common.TestCase): """ % {"uri": uris[0], "proxy_uri": proxy_uri} app = common.create_pitivi(proxyingStrategy=ProxyingStrategy.ALL) + app.recent_manager.remove_item = mock.Mock(return_value=True) proxy_manager = app.proxy_manager project_manager = app.project_manager medialib = medialibrary.MediaLibraryWidget(app) -- GitLab