diff --git a/Makefile b/Makefile index 2fc80ff5..b50d96d2 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ SSOBJS = $(OBJDIR)\solvespace.obj \ $(OBJDIR)\expr.obj \ $(OBJDIR)\constraint.obj \ $(OBJDIR)\draw.obj \ + $(OBJDIR)\toolbar.obj \ $(OBJDIR)\drawconstraint.obj \ $(OBJDIR)\file.obj \ $(OBJDIR)\undoredo.obj \ @@ -67,3 +68,8 @@ $(RES): win32/$(@B).rc icon.ico rc win32/$(@B).rc mv win32/$(@B).res $(OBJDIR)/$(@B).res +toolbar.cpp: $(OBJDIR)/icons.h + +$(OBJDIR)/icons.h: icons/* png2c.pl + perl png2c.pl > $(OBJDIR)/icons.h + diff --git a/draw.cpp b/draw.cpp index 1f59ff6a..cdfc13df 100644 --- a/draw.cpp +++ b/draw.cpp @@ -25,6 +25,13 @@ void GraphicsWindow::MouseMoved(double x, double y, bool leftDown, shiftDown = !shiftDown; } + if(SS.showToolbar) { + if(ToolbarMouseMoved((int)x, (int)y)) { + hover.Clear(); + return; + } + } + Point2d mp = { x, y }; // If the middle button is down, then mouse movement is used to pan and @@ -337,6 +344,10 @@ void GraphicsWindow::MouseLeftDown(double mx, double my) { if(GraphicsEditControlIsVisible()) return; HideTextEditControl(); + if(SS.showToolbar) { + if(ToolbarMouseDown((int)mx, (int)my)) return; + } + // Make sure the hover is up to date. MouseMoved(mx, my, false, false, false, false, false); orig.mouse.x = mx; @@ -682,6 +693,14 @@ void GraphicsWindow::MouseScroll(double x, double y, int delta) { InvalidateGraphics(); } +void GraphicsWindow::MouseLeave(void) { + // Un-hover everything when the mouse leaves our window. + hover.Clear(); + toolbarTooltipped = 0; + toolbarHovered = 0; + PaintGraphics(); +} + bool GraphicsWindow::Selection::Equals(Selection *b) { if(entity.v != b->entity.v) return false; if(constraint.v != b->constraint.v) return false; @@ -1006,5 +1025,10 @@ void GraphicsWindow::Paint(int w, int h) { for(i = 0; i < MAX_SELECTED; i++) { selection[i].Draw(); } + + // And finally the toolbar. + if(SS.showToolbar) { + ToolbarDraw(); + } } diff --git a/graphicswin.cpp b/graphicswin.cpp index 49db02cc..1da1df6b 100644 --- a/graphicswin.cpp +++ b/graphicswin.cpp @@ -43,6 +43,7 @@ const GraphicsWindow::MenuEntry GraphicsWindow::menu[] = { { 1, "Nearest &Iso View\tF2", MNU_NEAREST_ISO, F(2), mView }, { 1, NULL, 0, NULL }, { 1, "Show Text &Window\tTab", MNU_SHOW_TEXT_WND, '\t', mView }, +{ 1, "Show &Toolbar", MNU_SHOW_TOOLBAR, 0, mView }, { 1, NULL, 0, NULL }, { 1, "Dimensions in &Inches", MNU_UNITS_INCHES, 0, mView }, { 1, "Dimensions in &Millimeters", MNU_UNITS_MM, 0, mView }, @@ -395,6 +396,12 @@ void GraphicsWindow::MenuView(int id) { SS.GW.EnsureValidActives(); break; + case MNU_SHOW_TOOLBAR: + SS.showToolbar = !SS.showToolbar; + SS.GW.EnsureValidActives(); + PaintGraphics(); + break; + case MNU_UNITS_MM: SS.viewUnits = SolveSpace::UNIT_MM; SS.later.showTW = true; @@ -475,6 +482,8 @@ void GraphicsWindow::EnsureValidActives(void) { ShowTextWindow(SS.GW.showTextWindow); CheckMenuById(MNU_SHOW_TEXT_WND, SS.GW.showTextWindow); + CheckMenuById(MNU_SHOW_TOOLBAR, SS.showToolbar); + if(change) SS.later.showTW = true; } diff --git a/icons/perpendicular.png b/icons/perpendicular.png index 8782fd53..b93d8b39 100644 Binary files a/icons/perpendicular.png and b/icons/perpendicular.png differ diff --git a/png2c.pl b/png2c.pl new file mode 100644 index 00000000..fa3a3a10 --- /dev/null +++ b/png2c.pl @@ -0,0 +1,35 @@ +#!/usr/bin/perl + +use GD; + +for $file () { + + $file =~ m#.*/(.*)\.png#; + $base = "Icon_$1"; + $base =~ y/-/_/; + + open(PNG, $file) or die "$file: $!\n"; + $img = newFromPng GD::Image(\*PNG) or die; + $img->trueColor(1); + + close PNG; + + ($width, $height) = $img->getBounds(); + die "$file: $width, $height" if ($width != 24) or ($height != 24); + + print "unsigned char $base\[24*24*3] = {\n"; + + for($y = 0; $y < 24; $y++) { + for($x = 0; $x < 24; $x++) { + $index = $img->getPixel($x, 23-$y); + ($r, $g, $b) = $img->rgb($index); + if($r + $g + $b < 11) { + ($r, $g, $b) = (30, 30, 30); + } + printf " 0x%02x, 0x%02x, 0x%02x,\n", $r, $g, $b; + } + } + + print "};\n\n"; + +} diff --git a/solvespace.cpp b/solvespace.cpp index 52b73be9..656a1264 100644 --- a/solvespace.cpp +++ b/solvespace.cpp @@ -53,6 +53,8 @@ void SolveSpace::Init(char *cmdLine) { exportOffset = CnfThawFloat(0.0f, "ExportOffset"); // Draw back faces of triangles (when mesh is leaky/self-intersecting) drawBackFaces = CnfThawDWORD(1, "DrawBackFaces"); + // Show toolbar in the graphics window + showToolbar = CnfThawDWORD(1, "ShowToolbar"); // Recent files menus for(i = 0; i < MAX_RECENT; i++) { char name[100]; @@ -115,6 +117,8 @@ void SolveSpace::Exit(void) { CnfFreezeFloat(exportOffset, "ExportOffset"); // Draw back faces of triangles (when mesh is leaky/self-intersecting) CnfFreezeDWORD(drawBackFaces, "DrawBackFaces"); + // Show toolbar in the graphics window + CnfFreezeDWORD(showToolbar, "ShowToolbar"); ExitNow(); } diff --git a/solvespace.h b/solvespace.h index 468021c1..28896e98 100644 --- a/solvespace.h +++ b/solvespace.h @@ -99,8 +99,12 @@ void dbp(char *str, ...); void SetWindowTitle(char *str); void Message(char *str, ...); void Error(char *str, ...); +void SetTimerFor(int milliseconds); void ExitNow(void); +void DrawWithBitmapFont(char *str); +void GetBitmapFontExtent(char *str, int *w, int *h); + void CnfFreezeString(char *str, char *name); void CnfFreezeDWORD(DWORD v, char *name); void CnfFreezeFloat(float v, char *name); @@ -372,6 +376,7 @@ public: float exportScale; float exportOffset; int drawBackFaces; + int showToolbar; int CircleSides(double r); typedef enum { diff --git a/toolbar.cpp b/toolbar.cpp new file mode 100644 index 00000000..1ff8edbe --- /dev/null +++ b/toolbar.cpp @@ -0,0 +1,263 @@ +#include "solvespace.h" +#include "obj/icons.h" + +BYTE SPACER[1]; +static const struct { + BYTE *image; + int menu; + char *tip; +} Toolbar[] = { + { Icon_line, GraphicsWindow::MNU_LINE_SEGMENT, "Sketch line segment" }, + { Icon_rectangle, GraphicsWindow::MNU_RECTANGLE, "Sketch rectangle" }, + { Icon_circle, GraphicsWindow::MNU_CIRCLE, "Sketch circle" }, + { Icon_arc, GraphicsWindow::MNU_ARC, "Sketch arc, or tangent arc at selected point" }, + { Icon_bezier, GraphicsWindow::MNU_CUBIC, "Sketch cubic Bezier section" }, + { Icon_point, GraphicsWindow::MNU_DATUM_POINT, "Sketch datum point" }, + { Icon_construction, GraphicsWindow::MNU_CONSTRUCTION, "Toggle construction" }, + { Icon_trim, GraphicsWindow::MNU_CONSTRUCTION, "Split lines / curves where they intersect" }, + { SPACER }, + + { Icon_length, GraphicsWindow::MNU_DISTANCE_DIA, "Constrain distance / diameter / length" }, + { Icon_angle, GraphicsWindow::MNU_ANGLE, "Constrain angle" }, + { Icon_horiz, GraphicsWindow::MNU_HORIZONTAL, "Constrain to be horizontal" }, + { Icon_vert, GraphicsWindow::MNU_VERTICAL, "Constrain to be vertical" }, + { Icon_parallel, GraphicsWindow::MNU_PARALLEL, "Constrain to be parallel or tangent" }, + { Icon_perpendicular, GraphicsWindow::MNU_PERPENDICULAR, "Constrain to be perpendicular" }, + { Icon_pointonx, GraphicsWindow::MNU_ON_ENTITY, "Constrain point on line / curve / plane / face" }, + { Icon_symmetric, GraphicsWindow::MNU_SYMMETRIC, "Constrain symmetric" }, + { Icon_ref, GraphicsWindow::MNU_REFERENCE, "Toggle reference dimension" }, + { SPACER }, + + { Icon_extrude, GraphicsWindow::MNU_GROUP_EXTRUDE, "New group extruding active sketch" }, + { Icon_sketch_in_plane, GraphicsWindow::MNU_GROUP_WRKPL, "New group in new workplane (thru given entities)" }, + { Icon_sketch_in_3d, GraphicsWindow::MNU_GROUP_3D, "New group in 3d" }, + { Icon_assemble, GraphicsWindow::MNU_GROUP_IMPORT, "New group importing / assembling file" }, + { SPACER }, + + { Icon_in3d, GraphicsWindow::MNU_FREE_IN_3D, "Sketch / constrain in 3d" }, + { Icon_ontoworkplane, GraphicsWindow::MNU_SEL_WORKPLANE, "Sketch / constrain in workplane" }, + { NULL }, +}; + +void GraphicsWindow::ToolbarDraw(void) { + ToolbarDrawOrHitTest(0, 0, true, NULL); +} + +bool GraphicsWindow::ToolbarMouseMoved(int x, int y) { + x += ((int)width/2); + y += ((int)height/2); + + int nh; + bool withinToolbar = ToolbarDrawOrHitTest(x, y, false, &nh); + if(!withinToolbar) nh = 0; + + if(nh != toolbarTooltipped) { + // Don't let the tool tip move around if the mouse moves within the + // same item. + toolbarMouseX = x; + toolbarMouseY = y; + toolbarTooltipped = 0; + } + + if(nh != toolbarHovered) { + toolbarHovered = nh; + SetTimerFor(1000); + PaintGraphics(); + } + // So if we moved off the toolbar, then toolbarHovered is now equal to + // zero, so it doesn't matter if the tool tip timer expires. And if + // we moved from one item to another, we reset the timer, so also okay. + return withinToolbar; +} + +bool GraphicsWindow::ToolbarMouseDown(int x, int y) { + x += ((int)width/2); + y += ((int)height/2); + + int nh; + bool withinToolbar = ToolbarDrawOrHitTest(x, y, false, &nh); + if(withinToolbar) { + for(int i = 0; SS.GW.menu[i].level >= 0; i++) { + if(nh == SS.GW.menu[i].id) { + (SS.GW.menu[i].fn)((GraphicsWindow::MenuId)SS.GW.menu[i].id); + break; + } + } + } + return withinToolbar; +} + +bool GraphicsWindow::ToolbarDrawOrHitTest(int mx, int my, + bool paint, int *menu) +{ + int i; + int x = 17, y = (int)(height - 52); + + int fudge = 8; + int h = 32*12 + 3*16 + fudge; + int aleft = 0, aright = 66, atop = y+16+fudge/2, abot = y+16-h; + + bool withinToolbar = + (mx >= aleft && mx <= aright && my <= atop && my >= abot); + + if(!paint && !withinToolbar) { + // This gets called every MouseMove event, so return quickly. + return false; + } + + if(paint) { + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glTranslated(-1, -1, 0); + glScaled(2.0/width, 2.0/height, 0); + glDisable(GL_LIGHTING); + + double c = 30.0/255; + glColor4d(c, c, c, 1.0); + glBegin(GL_QUADS); + glVertex2d(aleft, atop); + glVertex2d(aleft, abot); + glVertex2d(aright, abot); + glVertex2d(aright, atop); + glEnd(); + } + + struct { + bool show; + char *str; + } toolTip = { false, NULL }; + + bool leftpos = true; + for(i = 0; Toolbar[i].image; i++) { + if(Toolbar[i].image == SPACER) { + if(!leftpos) { + leftpos = true; + y -= 32; + x -= 32; + } + y -= 16; + + if(paint) { + // Draw a separator bar in a slightly different color. + int divw = 30, divh = 2; + glColor4d(0.17, 0.17, 0.17, 1); + x += 16; + y += 24; + glBegin(GL_QUADS); + glVertex2d(x+divw, y+divh); + glVertex2d(x+divw, y-divh); + glVertex2d(x-divw, y-divh); + glVertex2d(x-divw, y+divh); + glEnd(); + x -= 16; + y -= 24; + } + + continue; + } + + if(paint) { + glRasterPos2i(x - 12, y - 12); + glDrawPixels(24, 24, GL_RGB, GL_UNSIGNED_BYTE, Toolbar[i].image); + + if(toolbarHovered == Toolbar[i].menu) { + // Highlight the hovered or pending item. + glColor4d(1, 1, 0, 0.3); + int boxhw = 15; + glBegin(GL_QUADS); + glVertex2d(x+boxhw, y+boxhw); + glVertex2d(x+boxhw, y-boxhw); + glVertex2d(x-boxhw, y-boxhw); + glVertex2d(x-boxhw, y+boxhw); + glEnd(); + } + + if(toolbarTooltipped == Toolbar[i].menu) { + // Display the tool tip for this item; postpone till later + // so that no one draws over us. Don't need position since + // that's just wherever the mouse is. + toolTip.show = true; + toolTip.str = Toolbar[i].tip; + } + } else { + int boxhw = 16; + if(mx < (x+boxhw) && mx > (x - boxhw) && + my < (y+boxhw) && my > (y - boxhw)) + { + if(menu) *menu = Toolbar[i].menu; + } + } + + if(leftpos) { + x += 32; + leftpos = false; + } else { + x -= 32; + y -= 32; + leftpos = true; + } + } + + if(paint) { + // Do this last so that nothing can draw over it. + if(toolTip.show) { + char str[1024]; + if(strlen(toolTip.str) >= 200) oops(); + strcpy(str, toolTip.str); + + for(i = 0; SS.GW.menu[i].level >= 0; i++) { + if(toolbarTooltipped == SS.GW.menu[i].id) { + int accel = SS.GW.menu[i].accel; + int ac = accel & 0xff; + if(isalnum(ac) || ac == '[') { + char *s = str+strlen(str); + if(accel & 0x100) { + sprintf(s, " (Shift+%c)", ac); + } else if((accel & ~0xff) == 0) { + sprintf(s, " (%c)", ac); + } + } + break; + } + } + + int tw, th; + GetBitmapFontExtent(str, &tw, &th); + tw += 10; + th += 2; + + double ox = toolbarMouseX + 3, oy = toolbarMouseY + 3; + glColor4d(1.0, 1.0, 0.6, 1.0); + glBegin(GL_QUADS); + glVertex2d(ox, oy); + glVertex2d(ox+tw, oy); + glVertex2d(ox+tw, oy+th); + glVertex2d(ox, oy+th); + glEnd(); + glColor4d(0.0, 0.0, 0.0, 1.0); + glBegin(GL_LINE_LOOP); + glVertex2d(ox, oy); + glVertex2d(ox+tw, oy); + glVertex2d(ox+tw, oy+th); + glVertex2d(ox, oy+th); + glEnd(); + + glColor4d(0, 0, 0, 1); + glPushMatrix(); + glRasterPos2d(ox+6, oy+6); + DrawWithBitmapFont(str); + glPopMatrix(); + } + glxDepthRangeLockToFront(false); + } + + return withinToolbar; +} + +void GraphicsWindow::TimerCallback(void) { + SS.GW.toolbarTooltipped = SS.GW.toolbarHovered; + PaintGraphics(); +} + diff --git a/ui.h b/ui.h index 157b94f9..c2f4cd24 100644 --- a/ui.h +++ b/ui.h @@ -187,6 +187,7 @@ public: MNU_NEAREST_ORTHO, MNU_NEAREST_ISO, MNU_SHOW_TEXT_WND, + MNU_SHOW_TOOLBAR, MNU_UNITS_INCHES, MNU_UNITS_MM, // Edit @@ -371,6 +372,16 @@ public: void ClearSuper(void); + // The toolbar, in toolbar.cpp + bool ToolbarDrawOrHitTest(int x, int y, bool paint, int *menu); + void ToolbarDraw(void); + bool ToolbarMouseMoved(int x, int y); + bool ToolbarMouseDown(int x, int y); + static void TimerCallback(void); + int toolbarHovered; + int toolbarTooltipped; + int toolbarMouseX, toolbarMouseY; + // This sets what gets displayed. bool showWorkplanes; bool showNormals; @@ -395,6 +406,7 @@ public: void MouseLeftDoubleClick(double x, double y); void MouseMiddleOrRightDown(double x, double y); void MouseScroll(double x, double y, int delta); + void MouseLeave(void); void EditControlDone(char *s); }; diff --git a/win32/w32main.cpp b/win32/w32main.cpp index b997bdd5..02b08520 100644 --- a/win32/w32main.cpp +++ b/win32/w32main.cpp @@ -22,6 +22,9 @@ #define EDIT_WIDTH 220 #define EDIT_HEIGHT 21 +// The list representing glyph with ASCII code zero, for bitmap fonts +#define BITMAP_GLYPH_BASE 1000 + HINSTANCE Instance; HWND TextWnd; @@ -95,6 +98,27 @@ void Message(char *str, ...) va_end(f); } +void CALLBACK TimerCallback(HWND hwnd, UINT msg, UINT_PTR id, DWORD time) +{ + SS.GW.TimerCallback(); +} +void SetTimerFor(int milliseconds) +{ + SetTimer(GraphicsWnd, 1, milliseconds, TimerCallback); +} + +void DrawWithBitmapFont(char *str) +{ + // These lists were created in CreateGlContext + glListBase(BITMAP_GLYPH_BASE); + glCallLists(strlen(str), GL_UNSIGNED_BYTE, str); +} +void GetBitmapFontExtent(char *str, int *w, int *h) +{ + // Easy since that's a fixed-width font for now. + *h = TEXT_HEIGHT; + *w = TEXT_WIDTH*strlen(str); +} void OpenWebsite(char *url) { ShellExecute(GraphicsWnd, "open", url, NULL, NULL, SW_SHOWNORMAL); @@ -598,6 +622,10 @@ static void CreateGlContext(void) GraphicsHpgl = wglCreateContext(hdc); wglMakeCurrent(hdc, GraphicsHpgl); + + // Create a bitmap font in a display list, for DrawWithBitmapFont(). + SelectObject(hdc, FixedFont); + wglUseFontBitmaps(hdc, 0, 255, BITMAP_GLYPH_BASE); } void InvalidateGraphics(void) @@ -702,6 +730,10 @@ LRESULT CALLBACK GraphicsWndProc(HWND hwnd, UINT msg, WPARAM wParam, break; } + case WM_MOUSELEAVE: + SS.GW.MouseLeave(); + break; + case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: @@ -711,6 +743,15 @@ LRESULT CALLBACK GraphicsWndProc(HWND hwnd, UINT msg, WPARAM wParam, int x = LOWORD(lParam); int y = HIWORD(lParam); + // We need this in order to get the WM_MOUSELEAVE + TRACKMOUSEEVENT tme; + ZERO(&tme); + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = GraphicsWnd; + TrackMouseEvent(&tme); + + // Convert to xy (vs. ij) style coordinates, with (0, 0) at center RECT r; GetClientRect(GraphicsWnd, &r); x = x - (r.right - r.left)/2;