bsnes/hiro/gtk/window.cpp
Tim Allen 427bac3011 Update to v101r06 release.
byuu says:

I reworked the video sizing code. Ended up wasting five fucking hours
fighting GTK. When you call `gtk_widget_set_size_request`, it doesn't
actually happen then. This is kind of a big deal because when I then go
to draw onto the viewport, the actual viewport child window is still the
old size, so the image gets distorted. It recovers in a frame or so with
emulation, but if we were to put a still image on there, it would stay
distorted.

The first thought is, `while(gtk_events_pending())
gtk_main_iteration_do(false);` right after the `set_size_request`. But
nope, it tells you there's no events pending. So then you think, go
deeper, use `XPending()` instead. Same thing, GTK hasn't actually issued
the command to Xlib yet. So then you think, if the widget is realized,
just call a blocking `gtk_main_iteration`. One call does nothing, two
calls results in a deadlock on the second one ... do it before program
startup, and the main window will never appear. Great.

Oh, and it's not just the viewport. It's also the widget container area
of the windows, as well as the window itself, as well as the fullscreen
mode toggle effect. They all do this.

For the latter three, I couldn't find anything that worked, so I just
added 20ms loops of constantly calling `gtk_main_iteration_do(false)`
after each one of those things. The downside here is toggling the status
bar takes 40ms, so you'll see it and it'll feel a tiny bit sluggish.

But I can't have a 20ms wait on each widget resize, that would be
catastrophic to performance on windows with lots of widgets.

I tried hooking configure-event and size-allocate, but they were very
unreliable. So instead I ended up with a loop that waits up to a maximm
of 20ms that inspects the `widget->allocation.(width,height)` values
directly and waits for them to be what we asked for with
`set_size_request`.

There was some extreme ugliness in GTK with calling
`gtk_main_iteration_do` recursively (`hiro::Widget::setGeometry` is
called recursively), so I had to lock it to only happen on the top level
widgets (the child ones should get resized while waiting on the
top-level ones, so it should be fine in practice), and also only run it
on realized widgets.

Even still, I'm getting ~3 timeouts when opening the settings dialog in
higan, but no other windows. But, this is the best I can do for now.

And the reason for all of this pain? Yeah, updated the video code.

So the Emulator::Interface now has this:

    struct VideoSize { uint width, height; };  //or requiem for a tuple
    auto videoSize() -> VideoSize;
    auto videoSize(uint width, uint height, bool arc) -> VideoSize;

The first function, for now, is just returning the literal surface size.
I may remove this ... one thing I want to allow for is cores that send
different texture sizes based on interlace/hires/overscan/etc settings.

The second function is more interesting. Instead of having the UI trying
to figure out sizing, I figure the emulation cores can do a better job
and we can customize it per-core now. So it gets the window's width and
height, and whether the user asked for aspect correction, and then
computes the best width/height ratio possible. For now they're all just
doing multiples of a 1x scale to the UI 2x,3x,4x modes.

We still need a third function, which will probably be what I repurpose
videoSize() for: to return the 'effective' size for pixel shaders, to
then feed into ruby, to then feed into quark, to then feed into our
shaders. Since shaders use normalized coordinates for pixel fetching,
this should work out just fine. The real texture size will be exposed to
quark shaders as well, of course.

Now for the main window ... it's just hard-coded to be 640x480, 960x720,
1280x960 for now. It works nicely for some cores on some modes, not so
much for others. Work in progress I guess.

I also took the opportunity to draw the about dialog box logo on the
main window. Got a bit fancy and used the old spherical gradient and
impose functionality of nall/image on it. Very minor highlight, nothing
garish. Just something nicer than a solid black window.

If you guys want to mess around with sizes, placements, and gradient
styles/colors/shapes ... feel free. If you come up with something nicer,
do share.

That's what led to all the GTK hell ... the logo wasn't drawing right as
you resized the window. But now it is, though I am not at all happy with
the hacking I had to do.

I also had to improve the video update code as a result of this:

  - when you unload a game, it blacks out the screen
      - if you are not quitting the emulator, it'll draw the logo; if
        you are, it won't
  - when you load a game, it black out the logo

These options prevent any unsightliness from resizing the viewport with
image data on it already

I need to redraw the logo when toggling fullscreen with no game loaded
as well for Windows, it seems.
2016-08-15 14:52:05 +10:00

428 lines
14 KiB
C++

#if defined(Hiro_Window)
namespace hiro {
static auto Window_close(GtkWidget* widget, GdkEvent* event, pWindow* p) -> signed {
if(p->state().onClose) {
p->self().doClose();
} else {
p->self().setVisible(false);
}
if(p->state().modal && !p->pObject::state().visible) p->self().setModal(false);
return true;
}
static auto Window_expose(GtkWidget* widget, GdkEvent* event, pWindow* p) -> signed {
if(auto color = p->state().backgroundColor) {
cairo_t* context = gdk_cairo_create(widget->window);
double red = (double)color.red() / 255.0;
double green = (double)color.green() / 255.0;
double blue = (double)color.blue() / 255.0;
double alpha = (double)color.alpha() / 255.0;
if(gdk_screen_is_composited(gdk_screen_get_default())
&& gdk_screen_get_rgba_colormap(gdk_screen_get_default())
) {
cairo_set_source_rgba(context, red, green, blue, alpha);
} else {
cairo_set_source_rgb(context, red, green, blue);
}
cairo_set_operator(context, CAIRO_OPERATOR_SOURCE);
cairo_paint(context);
cairo_destroy(context);
}
return false;
}
static auto Window_configure(GtkWidget* widget, GdkEvent* event, pWindow* p) -> signed {
if(!gtk_widget_get_realized(p->widget)) return false;
if(!p->pObject::state().visible) return false;
GdkWindow* gdkWindow = gtk_widget_get_window(widget);
GdkRectangle border, client;
gdk_window_get_frame_extents(gdkWindow, &border);
gdk_window_get_geometry(gdkWindow, nullptr, nullptr, &client.width, &client.height, nullptr);
gdk_window_get_origin(gdkWindow, &client.x, &client.y);
if(!p->state().fullScreen) {
//update geometry settings
settings.geometry.frameX = client.x - border.x;
settings.geometry.frameY = client.y - border.y;
settings.geometry.frameWidth = border.width - client.width;
settings.geometry.frameHeight = border.height - client.height;
}
Geometry geometry = {
client.x,
client.y + p->_menuHeight(),
client.width,
client.height - p->_menuHeight() - p->_statusHeight()
};
//move
if(geometry.x() != p->state().geometry.x() || geometry.y() != p->state().geometry.y()) {
if(!p->state().fullScreen) {
p->state().geometry.setPosition({geometry.x(), geometry.y()});
}
if(!p->locked()) p->self().doMove();
}
//size
if(geometry.width() != p->state().geometry.width() || geometry.height() != p->state().geometry.height()) {
p->onSizePending = true;
}
return false;
}
static auto Window_drop(GtkWidget* widget, GdkDragContext* context, signed x, signed y,
GtkSelectionData* data, unsigned type, unsigned timestamp, pWindow* p) -> void {
if(!p->state().droppable) return;
auto paths = DropPaths(data);
if(!paths) return;
p->self().doDrop(paths);
}
static auto Window_keyPress(GtkWidget* widget, GdkEventKey* event, pWindow* p) -> signed {
if(auto key = pKeyboard::_translate(event->keyval)) {
p->self().doKeyPress(key);
}
return false;
}
static auto Window_keyRelease(GtkWidget* widget, GdkEventKey* event, pWindow* p) -> signed {
if(auto key = pKeyboard::_translate(event->keyval)) {
p->self().doKeyRelease(key);
}
return false;
}
static auto Window_sizeAllocate(GtkWidget* widget, GtkAllocation* allocation, pWindow* p) -> void {
//size-allocate sent from gtk_fixed_move(); detect if layout unchanged and return
if(allocation->width == p->lastAllocation.width
&& allocation->height == p->lastAllocation.height) return;
if(!p->state().fullScreen) {
p->state().geometry.setSize({allocation->width, allocation->height});
}
if(auto& layout = p->state().layout) {
layout->setGeometry(p->self().geometry().setPosition(0, 0));
}
if(!p->locked() && p->onSizePending) {
p->onSizePending = false;
p->self().doSize();
}
p->lastAllocation = *allocation;
}
static auto Window_sizeRequest(GtkWidget* widget, GtkRequisition* requisition, pWindow* p) -> void {
requisition->width = p->state().geometry.width();
requisition->height = p->state().geometry.height();
}
auto pWindow::construct() -> void {
lastAllocation.width = 0;
lastAllocation.height = 0;
onSizePending = false;
widget = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_resizable(GTK_WINDOW(widget), true);
//if program was given a name, try and set the window taskbar icon from one of the pixmaps folders
if(!Application::state.name);
else if(_setIcon({Path::user(), ".local/share/icons/"}));
else if(_setIcon("/usr/local/share/pixmaps/"));
else if(_setIcon("/usr/share/pixmaps/"));
GdkColormap* colormap = gdk_screen_get_rgba_colormap(gdk_screen_get_default());
if(!colormap) colormap = gdk_screen_get_rgb_colormap(gdk_screen_get_default());
if(colormap) gtk_widget_set_colormap(widget, colormap);
gtk_widget_set_app_paintable(widget, true);
gtk_widget_add_events(widget, GDK_CONFIGURE);
menuContainer = gtk_vbox_new(false, 0);
gtk_container_add(GTK_CONTAINER(widget), menuContainer);
gtk_widget_show(menuContainer);
gtkMenu = gtk_menu_bar_new();
gtk_box_pack_start(GTK_BOX(menuContainer), gtkMenu, false, false, 0);
formContainer = gtk_fixed_new();
gtk_box_pack_start(GTK_BOX(menuContainer), formContainer, true, true, 0);
gtk_widget_show(formContainer);
statusContainer = gtk_event_box_new();
gtkStatus = gtk_statusbar_new();
gtk_statusbar_set_has_resize_grip(GTK_STATUSBAR(gtkStatus), true);
gtk_container_add(GTK_CONTAINER(statusContainer), gtkStatus);
gtk_box_pack_start(GTK_BOX(menuContainer), statusContainer, false, false, 0);
gtk_widget_show(statusContainer);
setBackgroundColor(state().backgroundColor);
setDroppable(state().droppable);
setGeometry(state().geometry);
setResizable(state().resizable);
setTitle(state().title);
g_signal_connect(G_OBJECT(widget), "delete-event", G_CALLBACK(Window_close), (gpointer)this);
g_signal_connect(G_OBJECT(widget), "expose-event", G_CALLBACK(Window_expose), (gpointer)this);
g_signal_connect(G_OBJECT(widget), "configure-event", G_CALLBACK(Window_configure), (gpointer)this);
g_signal_connect(G_OBJECT(widget), "drag-data-received", G_CALLBACK(Window_drop), (gpointer)this);
g_signal_connect(G_OBJECT(widget), "key-press-event", G_CALLBACK(Window_keyPress), (gpointer)this);
g_signal_connect(G_OBJECT(widget), "key-release-event", G_CALLBACK(Window_keyRelease), (gpointer)this);
g_signal_connect(G_OBJECT(formContainer), "size-allocate", G_CALLBACK(Window_sizeAllocate), (gpointer)this);
g_signal_connect(G_OBJECT(formContainer), "size-request", G_CALLBACK(Window_sizeRequest), (gpointer)this);
}
auto pWindow::destruct() -> void {
gtk_widget_destroy(widget);
}
auto pWindow::append(sLayout layout) -> void {
}
auto pWindow::append(sMenuBar menuBar) -> void {
_setMenuEnabled(menuBar->enabled(true));
_setMenuFont(menuBar->font(true));
_setMenuVisible(menuBar->visible(true));
}
auto pWindow::append(sStatusBar statusBar) -> void {
_setStatusEnabled(statusBar->enabled(true));
_setStatusFont(statusBar->font(true));
_setStatusText(statusBar->text());
_setStatusVisible(statusBar->visible(true));
}
auto pWindow::focused() const -> bool {
return gtk_window_is_active(GTK_WINDOW(widget));
}
auto pWindow::frameMargin() const -> Geometry {
if(state().fullScreen) return {
0, _menuHeight(),
0, _menuHeight() + _statusHeight()
};
return {
settings.geometry.frameX,
settings.geometry.frameY + _menuHeight(),
settings.geometry.frameWidth,
settings.geometry.frameHeight + _menuHeight() + _statusHeight()
};
}
auto pWindow::remove(sLayout layout) -> void {
}
auto pWindow::remove(sMenuBar menuBar) -> void {
_setMenuVisible(false);
}
auto pWindow::remove(sStatusBar statusBar) -> void {
_setStatusVisible(false);
}
auto pWindow::setBackgroundColor(Color color) -> void {
GdkColor gdkColor = CreateColor(color);
gtk_widget_modify_bg(widget, GTK_STATE_NORMAL, color ? &gdkColor : nullptr);
}
auto pWindow::setDroppable(bool droppable) -> void {
gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, nullptr, 0, GDK_ACTION_COPY);
if(droppable) gtk_drag_dest_add_uri_targets(widget);
}
auto pWindow::setEnabled(bool enabled) -> void {
if(auto& menuBar = state().menuBar) {
if(auto self = menuBar->self()) self->setEnabled(menuBar->enabled(true));
}
if(auto& statusBar = state().statusBar) {
if(auto self = statusBar->self()) self->setEnabled(statusBar->enabled(true));
}
if(auto& layout = state().layout) {
if(auto self = layout->self()) self->setEnabled(layout->enabled(true));
}
}
auto pWindow::setFocused() -> void {
gtk_window_present(GTK_WINDOW(widget));
}
auto pWindow::setFullScreen(bool fullScreen) -> void {
if(fullScreen) {
windowedGeometry = state().geometry;
gtk_window_fullscreen(GTK_WINDOW(widget));
state().geometry = Monitor::geometry(Monitor::primary());
} else {
gtk_window_unfullscreen(GTK_WINDOW(widget));
state().geometry = windowedGeometry;
}
auto time = chrono::millisecond();
while(chrono::millisecond() - time < 20) gtk_main_iteration_do(false);
}
auto pWindow::setGeometry(Geometry geometry) -> void {
Geometry margin = frameMargin();
gtk_window_move(GTK_WINDOW(widget), geometry.x() - margin.x(), geometry.y() - margin.y());
GdkGeometry geom;
geom.min_width = state().resizable ? 1 : state().geometry.width();
geom.min_height = state().resizable ? 1 : state().geometry.height();
gtk_window_set_geometry_hints(GTK_WINDOW(widget), GTK_WIDGET(widget), &geom, GDK_HINT_MIN_SIZE);
gtk_widget_set_size_request(formContainer, geometry.width(), geometry.height());
auto time1 = chrono::millisecond();
while(chrono::millisecond() - time1 < 20) gtk_main_iteration_do(false);
gtk_window_resize(GTK_WINDOW(widget), geometry.width(), geometry.height() + _menuHeight() + _statusHeight());
auto time2 = chrono::millisecond();
while(chrono::millisecond() - time2 < 20) gtk_main_iteration_do(false);
}
auto pWindow::setModal(bool modal) -> void {
if(modal) {
gtk_window_set_modal(GTK_WINDOW(widget), true);
while(!Application::state.quit && state().modal) {
if(Application::state.onMain) {
Application::doMain();
} else {
usleep(20 * 1000);
}
Application::processEvents();
}
gtk_window_set_modal(GTK_WINDOW(widget), false);
}
}
auto pWindow::setResizable(bool resizable) -> void {
gtk_window_set_resizable(GTK_WINDOW(widget), resizable);
gtk_statusbar_set_has_resize_grip(GTK_STATUSBAR(gtkStatus), resizable);
}
auto pWindow::setTitle(const string& title) -> void {
gtk_window_set_title(GTK_WINDOW(widget), title ? title : " ");
}
auto pWindow::setVisible(bool visible) -> void {
gtk_widget_set_visible(widget, visible);
if(auto& menuBar = state().menuBar) {
if(menuBar->self()) menuBar->self()->setVisible(menuBar->visible(true));
}
if(auto& layout = state().layout) {
if(layout->self()) layout->self()->setVisible(layout->visible(true));
}
if(auto& statusBar = state().statusBar) {
if(statusBar->self()) statusBar->self()->setVisible(statusBar->visible(true));
}
//GTK+ returns invalid widget sizes below without this call
Application::processEvents();
if(visible) {
if(gtk_widget_get_visible(gtkMenu)) {
GtkAllocation allocation;
gtk_widget_get_allocation(gtkMenu, &allocation);
settings.geometry.menuHeight = allocation.height;
}
if(gtk_widget_get_visible(gtkStatus)) {
GtkAllocation allocation;
gtk_widget_get_allocation(gtkStatus, &allocation);
settings.geometry.statusHeight = allocation.height;
}
}
if(auto& layout = state().layout) {
layout->setGeometry(self().geometry().setPosition(0, 0));
}
}
auto pWindow::_append(mWidget& widget) -> void {
if(!widget.self()) return;
if(auto parent = widget.parentWidget(true)) {
if(auto instance = parent->self()) widget.self()->gtkParent = instance->container(widget);
} else {
widget.self()->gtkParent = formContainer;
}
gtk_fixed_put(GTK_FIXED(widget.self()->gtkParent), widget.self()->gtkWidget, 0, 0);
}
auto pWindow::_append(mMenu& menu) -> void {
if(menu.self()) gtk_menu_shell_append(GTK_MENU_SHELL(gtkMenu), menu.self()->widget);
}
auto pWindow::_menuHeight() const -> signed {
return gtk_widget_get_visible(gtkMenu) ? settings.geometry.menuHeight : 0;
}
auto pWindow::_setIcon(const string& pathname) -> bool {
string filename;
filename = {pathname, Application::state.name, ".svg"};
if(file::exists(filename)) {
gtk_window_set_icon_from_file(GTK_WINDOW(widget), filename, nullptr);
return true;
}
filename = {pathname, Application::state.name, ".png"};
if(file::exists(filename)) {
//maximum image size GTK+ supports is 256x256; scale image down if necessary to prevent error
image icon(filename);
icon.scale(min(256u, icon.width()), min(256u, icon.height()), true);
auto pixbuf = CreatePixbuf(icon);
gtk_window_set_icon(GTK_WINDOW(widget), pixbuf);
g_object_unref(G_OBJECT(pixbuf));
return true;
}
return false;
}
auto pWindow::_setMenuEnabled(bool enabled) -> void {
gtk_widget_set_sensitive(gtkMenu, enabled);
}
auto pWindow::_setMenuFont(const Font& font) -> void {
pFont::setFont(gtkMenu, font);
}
auto pWindow::_setMenuVisible(bool visible) -> void {
gtk_widget_set_visible(gtkMenu, visible);
}
auto pWindow::_setStatusEnabled(bool enabled) -> void {
gtk_widget_set_sensitive(gtkStatus, enabled);
}
auto pWindow::_setStatusFont(const Font& font) -> void {
pFont::setFont(gtkStatus, font);
}
auto pWindow::_setStatusText(const string& text) -> void {
gtk_statusbar_pop(GTK_STATUSBAR(gtkStatus), 1);
gtk_statusbar_push(GTK_STATUSBAR(gtkStatus), 1, text);
}
auto pWindow::_setStatusVisible(bool visible) -> void {
gtk_widget_set_visible(gtkStatus, visible);
}
auto pWindow::_statusHeight() const -> signed {
return gtk_widget_get_visible(gtkStatus) ? settings.geometry.statusHeight : 0;
}
}
#endif